2 * shib-mysql-ccache.cpp: Shibboleth Credential Cache using MySQL.
4 * Created by: Derek Atkins <derek@ihtfp.com>
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
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
21 // eventually we might be able to support autoconf via cygwin...
22 #if defined (_MSC_VER) || defined(__BORLANDC__)
23 # include "config_win32.h"
29 # define SHIBMYSQL_EXPORTS __declspec(dllexport)
31 # define SHIBMYSQL_EXPORTS
38 #include <shib-target/shib-target.h>
39 #include <shib/shib-threads.h>
40 #include <log4cpp/Category.hh>
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)")
50 #ifdef HAVE_LIBDMALLOCXX
56 using namespace shibboleth;
57 using namespace shibtarget;
58 using namespace log4cpp;
60 #define PLUGIN_VER_MAJOR 1
61 #define PLUGIN_VER_MINOR 0
63 static const XMLCh Argument[] =
64 { chLatin_A, chLatin_r, chLatin_g, chLatin_u, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chNull };
65 static const XMLCh cleanupInterval[] =
66 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
67 chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
69 static const XMLCh cacheTimeout[] =
70 { 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 };
71 static const XMLCh mysqlTimeout[] =
72 { 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 };
74 class ShibMySQLCCache;
75 class ShibMySQLCCacheEntry : public ISessionCacheEntry
78 ShibMySQLCCacheEntry(const char*, ISessionCacheEntry*, ShibMySQLCCache*);
79 ~ShibMySQLCCacheEntry() {}
81 virtual void lock() {}
82 virtual void unlock() { m_cacheEntry->unlock(); delete this; }
83 virtual bool isValid(time_t lifetime, time_t timeout) const;
84 virtual const char* getClientAddress() const { return m_cacheEntry->getClientAddress(); }
85 virtual const SAMLAuthenticationStatement* getAuthnStatement() const { return m_cacheEntry->getAuthnStatement(); }
86 virtual Iterator<SAMLAssertion*> getAssertions() { return m_cacheEntry->getAssertions(); }
91 ShibMySQLCCache* m_cache;
92 ISessionCacheEntry* m_cacheEntry;
96 class ShibMySQLCCache : public ISessionCache
99 ShibMySQLCCache(const DOMElement* e);
100 virtual ~ShibMySQLCCache();
102 virtual void thread_init();
103 virtual void thread_end() {}
105 virtual string generateKey() const {return m_cache->generateKey();}
106 virtual ISessionCacheEntry* find(const char* key, const IApplication* application);
109 const IApplication* application,
110 SAMLAuthenticationStatement *s,
111 const char *client_addr,
112 SAMLResponse* r=NULL,
113 const IRoleDescriptor* source=NULL);
114 virtual void remove(const char* key);
117 MYSQL* getMYSQL() const;
119 log4cpp::Category* log;
122 ISessionCache* m_cache;
124 const DOMElement* m_root; // can only use this during initialization
126 static void* cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache
127 CondWait* shutdown_wait;
129 Thread* cleanup_thread;
133 void createDatabase(MYSQL*, int major, int minor);
134 void upgradeDatabase(MYSQL*);
135 void getVersion(MYSQL*, int* major_p, int* minor_p);
136 bool repairTable(MYSQL*&, const char* table);
139 // Forward declarations
140 extern "C" void shib_mysql_destroy_handle(void* data);
141 void mysqlInit(const DOMElement* e, Category& log);
143 /*************************************************************************
144 * The CCache here talks to a MySQL database. The database stores
145 * three items: the cookie (session key index), the lastAccess time, and
146 * the SAMLAuthenticationStatement. All other access is performed
147 * through the memory cache provided by shibboleth.
150 MYSQL* ShibMySQLCCache::getMYSQL() const
152 return (MYSQL*)m_mysql->getData();
155 void ShibMySQLCCache::thread_init()
158 saml::NDC ndc("thread_init");
161 // Connect to the database
162 MYSQL* mysql = mysql_init(NULL);
164 log->error("mysql_init failed");
166 throw SAMLException("ShibMySQLCCache::thread_init(): mysql_init() failed");
169 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
171 log->crit("mysql_real_connect failed: %s", mysql_error(mysql));
173 throw SAMLException("ShibMySQLCCache::thread_init(): mysql_real_connect() failed");
175 log->info("mysql_real_connect failed: %s. Trying to create", mysql_error(mysql));
177 // This will throw an exception if it fails.
178 createDatabase(mysql, PLUGIN_VER_MAJOR, PLUGIN_VER_MINOR);
182 int major = -1, minor = -1;
183 getVersion (mysql, &major, &minor);
185 // Make sure we've got the right version
186 if (major != PLUGIN_VER_MAJOR || minor != PLUGIN_VER_MINOR) {
188 // If we're capable, try upgrading on the fly...
189 if (major == 0 && minor == 0) {
190 upgradeDatabase(mysql);
194 log->crit("Invalid database version: %d.%d", major, minor);
195 throw SAMLException("ShibMySQLCCache::thread_init(): Invalid database version");
199 // We're all set.. Save off the handle for this thread.
200 m_mysql->setData(mysql);
203 ShibMySQLCCache::ShibMySQLCCache(const DOMElement* e)
206 saml::NDC ndc("shibmysql::ShibMySQLCCache");
209 m_mysql = ThreadKey::create(&shib_mysql_destroy_handle);
210 log = &(Category::getInstance("shibmysql::ShibMySQLCCache"));
218 m_cache = dynamic_cast<ISessionCache*>(
219 SAMLConfig::getConfig().getPlugMgr().newPlugin(
220 "edu.internet2.middleware.shibboleth.sp.provider.MemorySessionCacheProvider", e
224 // Initialize the cleanup thread
225 shutdown_wait = CondWait::create();
227 cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
230 ShibMySQLCCache::~ShibMySQLCCache()
233 shutdown_wait->signal();
234 cleanup_thread->join(NULL);
244 ISessionCacheEntry* ShibMySQLCCache::find(const char* key, const IApplication* application)
247 saml::NDC ndc("ShibMySQLCCache::find");
250 ISessionCacheEntry* res = m_cache->find(key, application);
253 log->debug("Looking in database...");
255 // nothing cached; see if this exists in the database
256 string q = string("SELECT application_id,addr,statement FROM state WHERE cookie='") + key + "' LIMIT 1";
259 MYSQL* mysql = getMYSQL();
260 if (mysql_query(mysql, q.c_str())) {
261 const char* err=mysql_error(mysql);
262 log->error("Error searching for %s: %s", key, err);
263 if (isCorrupt(err) && repairTable(mysql,"state")) {
264 if (mysql_query(mysql, q.c_str()))
265 log->error("Error retrying search for %s: %s", key, mysql_error(mysql));
269 rows = mysql_store_result(mysql);
271 // Nope, doesn't exist.
275 // Make sure we got 1 and only 1 rows.
276 if (mysql_num_rows(rows) != 1) {
277 log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows));
278 mysql_free_result(rows);
282 log->debug("Match found. Parsing...");
284 // Pull apart the row and process the results
285 MYSQL_ROW row = mysql_fetch_row(rows);
286 IConfig* conf=ShibTargetConfig::getConfig().getINI();
288 const IApplication* application=conf->getApplication(row[0]);
290 mysql_free_result(rows);
291 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"unable to locate application for session, deleted?");
293 else if (strcmp(row[0],application->getId())) {
294 log->crit("An application (%s) attempted to access another application's (%s) session!", application->getId(), row[0]);
295 mysql_free_result(rows);
299 istringstream str(row[2]);
300 SAMLAuthenticationStatement *s = NULL;
302 // Try to parse the AuthStatement
304 s = new SAMLAuthenticationStatement(str);
306 mysql_free_result(rows);
310 // Insert it into the memory cache
312 m_cache->insert(key, application, s, row[1]);
314 // Free the results, and then re-run the 'find' query
315 mysql_free_result(rows);
316 res = m_cache->find(key,application);
321 return new ShibMySQLCCacheEntry(key, res, this);
324 void ShibMySQLCCache::insert(
326 const IApplication* application,
327 saml::SAMLAuthenticationStatement *s,
328 const char *client_addr,
329 saml::SAMLResponse* r,
330 const IRoleDescriptor* source)
333 saml::NDC ndc("ShibMySQLCCache::insert");
338 string q = string("INSERT INTO state VALUES('") + key + "','" + application->getId() + "',NOW(),'" + client_addr + "','" + os.str() + "')";
340 log->debug("Query: %s", q.c_str());
342 // then add it to the database
343 MYSQL* mysql = getMYSQL();
344 if (mysql_query(mysql, q.c_str())) {
345 const char* err=mysql_error(mysql);
346 log->error("Error inserting %s: %s", key, err);
347 if (isCorrupt(err) && repairTable(mysql,"state")) {
349 if (mysql_query(mysql, q.c_str()))
350 log->error("Error inserting %s: %s", key, mysql_error(mysql));
351 throw SAMLException("ShibMySQLCCache::insert(): inset failed");
355 // Add it to the memory cache
356 m_cache->insert(key, application, s, client_addr, r, source);
359 void ShibMySQLCCache::remove(const char* key)
362 saml::NDC ndc("ShibMySQLCCache::remove");
365 // Remove the cached version
366 m_cache->remove(key);
368 // Remove from the database
369 string q = string("DELETE FROM state WHERE cookie='") + key + "'";
370 MYSQL* mysql = getMYSQL();
371 if (mysql_query(mysql, q.c_str())) {
372 const char* err=mysql_error(mysql);
373 log->error("Error deleting entry %s: %s", key, err);
374 if (isCorrupt(err) && repairTable(mysql,"state")) {
376 if (mysql_query(mysql, q.c_str()))
377 log->error("Error deleting entry %s: %s", key, mysql_error(mysql));
382 void ShibMySQLCCache::cleanup()
385 saml::NDC ndc("ShibMySQLCCache::cleanup");
388 Mutex* mutex = Mutex::create();
392 int timeout_life = 0;
394 // Load our configuration details...
395 const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
397 rerun_timer = XMLString::parseInt(tag);
399 // search for 'mysql-cache-timeout' and then the regular cache timeout
400 tag=m_root->getAttributeNS(NULL,mysqlTimeout);
402 timeout_life = XMLString::parseInt(tag);
404 tag=m_root->getAttributeNS(NULL,cacheTimeout);
406 timeout_life = XMLString::parseInt(tag);
409 if (rerun_timer <= 0)
410 rerun_timer = 300; // rerun every 5 minutes
412 if (timeout_life <= 0)
413 timeout_life = 28800; // timeout after 8 hours
417 MYSQL* mysql = getMYSQL();
419 while (shutdown == false) {
420 shutdown_wait->timedwait(mutex, rerun_timer);
422 if (shutdown == true)
425 // Find all the entries in the database that haven't been used
426 // recently In particular, find all entries that have not been
427 // accessed in 'timeout_life' seconds.
429 q << "SELECT cookie FROM state WHERE " <<
430 "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life;
433 if (mysql_query(mysql, q.str().c_str())) {
434 const char* err=mysql_error(mysql);
435 log->error("Error searching for old items: %s", err);
436 if (isCorrupt(err) && repairTable(mysql,"state")) {
437 if (mysql_query(mysql, q.str().c_str()))
438 log->error("Error re-searching for old items: %s", mysql_error(mysql));
442 rows = mysql_store_result(mysql);
446 if (mysql_num_fields(rows) != 1) {
447 log->error("Wrong number of rows, 1 != %d", mysql_num_fields(rows));
448 mysql_free_result(rows);
452 // For each row, remove the entry from the database.
454 while ((row = mysql_fetch_row(rows)) != NULL)
457 mysql_free_result(rows);
460 log->debug("cleanup thread exiting...");
468 void* ShibMySQLCCache::cleanup_fcn(void* cache_p)
470 ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p;
472 // First, let's block all signals
473 Thread::mask_all_signals();
475 // Now run the cleanup process.
480 bool ShibMySQLCCache::repairTable(MYSQL*& mysql, const char* table)
482 string q = string("REPAIR TABLE ") + table;
483 if (mysql_query(mysql, q.c_str())) {
484 log->error("Error repairing table %s: %s", table, mysql_error(mysql));
488 // seems we have to recycle the connection to get the thread to keep working
489 // other threads seem to be ok, but we should monitor that
491 m_mysql->setData(NULL);
497 void ShibMySQLCCache::createDatabase(MYSQL* mysql, int major, int minor)
499 log->info("Creating database.");
503 ms = mysql_init(NULL);
505 log->crit("mysql_init failed");
506 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_init failed");
509 if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
510 log->crit("cannot open DB file to create DB: %s", mysql_error(ms));
511 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect failed");
514 if (mysql_query(ms, "CREATE DATABASE shar")) {
515 log->crit("cannot create shar database: %s", mysql_error(ms));
516 throw SAMLException("ShibMySQLCCache::createDatabase(): create db cmd failed");
519 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
520 log->crit("cannot open SHAR database");
521 throw SAMLException("ShibMySQLCCache::createDatabase(): mysql_real_connect to shar db failed");
527 catch (SAMLException&) {
534 // Now create the tables if they don't exist
535 log->info("Creating database tables");
537 if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)")) {
538 log->error ("Error creating version: %s", mysql_error(mysql));
539 throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
542 if (mysql_query(mysql,
543 "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
544 "atime DATETIME, addr VARCHAR(128), statement TEXT)")) {
545 log->error ("Error creating state: %s", mysql_error(mysql));
546 throw SAMLException("ShibMySQLCCache::createDatabase(): create table cmd failed");
550 q << "INSERT INTO version VALUES(" << major << "," << minor << ")";
551 if (mysql_query(mysql, q.str().c_str())) {
552 log->error ("Error setting version: %s", mysql_error(mysql));
553 throw SAMLException("ShibMySQLCCache::createDatabase(): version insert failed");
557 void ShibMySQLCCache::upgradeDatabase(MYSQL* mysql)
559 if (mysql_query(mysql, "DROP TABLE state")) {
560 log->error("Error dropping old session state table: %s", mysql_error(mysql));
563 if (mysql_query(mysql,
564 "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
565 "atime DATETIME, addr VARCHAR(128), statement TEXT)")) {
566 log->error ("Error creating state table: %s", mysql_error(mysql));
567 throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error creating state table");
571 q << "UPDATE version SET major = " << PLUGIN_VER_MAJOR;
572 if (mysql_query(mysql, q.str().c_str())) {
573 log->error ("Error updating version: %s", mysql_error(mysql));
574 throw SAMLException("ShibMySQLCCache::upgradeDatabase(): error updating version");
578 void ShibMySQLCCache::getVersion(MYSQL* mysql, int* major_p, int* minor_p)
580 // grab the version number from the database
581 if (mysql_query(mysql, "SELECT * FROM version"))
582 log->error ("Error reading version: %s", mysql_error(mysql));
584 MYSQL_RES* rows = mysql_store_result(mysql);
586 if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2) {
587 MYSQL_ROW row = mysql_fetch_row(rows);
589 int major = row[0] ? atoi(row[0]) : -1;
590 int minor = row[1] ? atoi(row[1]) : -1;
591 log->debug("opening database version %d.%d", major, minor);
593 mysql_free_result (rows);
600 // Wrong number of rows or wrong number of fields...
602 log->crit("Houston, we've got a problem with the database...");
603 mysql_free_result (rows);
604 throw SAMLException("ShibMySQLCCache::getVersion(): version verification failed");
607 log->crit("MySQL Read Failed in version verificatoin");
608 throw SAMLException("ShibMySQLCCache::getVersion(): error reading version");
611 void mysqlInit(const DOMElement* e, Category& log)
613 static bool done = false;
615 log.info("MySQL embedded server already initialized");
618 log.info("initializing MySQL embedded server");
620 // Setup the argument array
621 vector<string> arg_array;
622 arg_array.push_back("shibboleth");
624 // grab any MySQL parameters from the config file
625 e=saml::XML::getFirstChildElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
627 auto_ptr_char arg(e->getFirstChild()->getNodeValue());
629 arg_array.push_back(arg.get());
630 e=saml::XML::getNextSiblingElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
633 // Compute the argument array
634 int arg_count = arg_array.size();
635 const char** args=new const char*[arg_count];
636 for (int i = 0; i < arg_count; i++)
637 args[i] = arg_array[i].c_str();
639 // Initialize MySQL with the arguments
640 mysql_server_init(arg_count, (char **)args, NULL);
646 /*************************************************************************
647 * The CCacheEntry here is mostly a wrapper around the "memory"
648 * cacheentry provided by shibboleth. The only difference is that we
649 * intercept the isSessionValid() so that we can "touch()" the
650 * database if the session is still valid.
653 ShibMySQLCCacheEntry::ShibMySQLCCacheEntry(const char* key, ISessionCacheEntry* entry, ShibMySQLCCache* cache)
655 m_cacheEntry = entry;
660 bool ShibMySQLCCacheEntry::isValid(time_t lifetime, time_t timeout) const
662 bool res = m_cacheEntry->isValid(lifetime, timeout);
668 bool ShibMySQLCCacheEntry::touch() const
670 string q=string("UPDATE state SET atime=NOW() WHERE cookie='") + m_key + "'";
672 MYSQL* mysql = m_cache->getMYSQL();
673 if (mysql_query(mysql, q.c_str())) {
674 m_cache->log->info("Error updating timestamp on %s: %s",
675 m_key.c_str(), mysql_error(mysql));
681 /*************************************************************************
682 * The registration functions here...
685 IPlugIn* new_mysql_ccache(const DOMElement* e)
687 return new ShibMySQLCCache(e);
690 IPlugIn* new_mysql_replay(const DOMElement* e)
695 #define REPLAYPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLReplayCacheProvider"
696 #define SESSIONPLUGINTYPE "edu.internet2.middleware.shibboleth.sp.provider.MySQLSessionCacheProvider"
698 extern "C" int SHIBMYSQL_EXPORTS saml_extension_init(void*)
700 // register this ccache type
701 SAMLConfig::getConfig().getPlugMgr().regFactory(REPLAYPLUGINTYPE, &new_mysql_replay);
702 SAMLConfig::getConfig().getPlugMgr().regFactory(SESSIONPLUGINTYPE, &new_mysql_ccache);
706 extern "C" void SHIBMYSQL_EXPORTS saml_extension_term()
708 SAMLConfig::getConfig().getPlugMgr().unregFactory(REPLAYPLUGINTYPE);
709 SAMLConfig::getConfig().getPlugMgr().unregFactory(SESSIONPLUGINTYPE);
712 /*************************************************************************
716 extern "C" void shib_mysql_destroy_handle(void* data)
718 MYSQL* mysql = (MYSQL*) data;