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 #ifdef HAVE_LIBDMALLOCXX
53 using namespace shibboleth;
54 using namespace shibtarget;
56 #define PLUGIN_VER_MAJOR 1
57 #define PLUGIN_VER_MINOR 0
59 static const XMLCh Argument[] =
60 { chLatin_A, chLatin_r, chLatin_g, chLatin_u, chLatin_m, chLatin_e, chLatin_n, chLatin_t, chNull };
61 static const XMLCh cleanupInterval[] =
62 { chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p,
63 chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull
65 static const XMLCh cacheTimeout[] =
66 { 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 };
67 static const XMLCh mysqlTimeout[] =
68 { 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 };
70 class ShibMySQLCCache;
71 class ShibMySQLCCacheEntry : public ISessionCacheEntry
74 ShibMySQLCCacheEntry(const char*, ISessionCacheEntry*, ShibMySQLCCache*);
75 ~ShibMySQLCCacheEntry() {}
77 virtual void lock() {}
78 virtual void unlock() { m_cacheEntry->unlock(); delete this; }
79 virtual bool isValid(time_t lifetime, time_t timeout) const;
80 virtual const char* getClientAddress() const { return m_cacheEntry->getClientAddress(); }
81 virtual const char* getSerializedStatement() const { return m_cacheEntry->getSerializedStatement(); }
82 virtual const SAMLAuthenticationStatement* getStatement() const { return m_cacheEntry->getStatement(); }
83 virtual Iterator<SAMLAssertion*> getAssertions() { return m_cacheEntry->getAssertions(); }
84 virtual void preFetch(int prefetch_window) { m_cacheEntry->preFetch(prefetch_window); }
89 ShibMySQLCCache* m_cache;
90 ISessionCacheEntry* m_cacheEntry;
94 class ShibMySQLCCache : public ISessionCache
97 ShibMySQLCCache(const DOMElement* e);
98 virtual ~ShibMySQLCCache();
100 virtual void thread_init();
101 virtual void thread_end() {}
103 virtual string generateKey() const {return m_cache->generateKey();}
104 virtual ISessionCacheEntry* find(const char* key);
107 const IApplication* application,
108 SAMLAuthenticationStatement *s,
109 const char *client_addr,
110 SAMLResponse* r=NULL);
111 virtual void remove(const char* key);
114 MYSQL* getMYSQL() const;
116 log4cpp::Category* log;
119 ISessionCache* m_cache;
121 const DOMElement* m_root; // can only use this during initialization
123 static void* cleanup_fcn(void*); // XXX Assumed an ShibMySQLCCache
124 CondWait* shutdown_wait;
126 Thread* cleanup_thread;
130 void createDatabase(MYSQL*, int major, int minor);
131 void upgradeDatabase(MYSQL*);
132 void getVersion(MYSQL*, int* major_p, int* minor_p);
133 void mysqlInit(void);
136 // Forward declarations
137 extern "C" void shib_mysql_destroy_handle(void* data);
139 /*************************************************************************
140 * The CCache here talks to a MySQL database. The database stores
141 * three items: the cookie (session key index), the lastAccess time, and
142 * the SAMLAuthenticationStatement. All other access is performed
143 * through the memory cache provided by shibboleth.
146 MYSQL* ShibMySQLCCache::getMYSQL() const
148 void* data = m_mysql->getData();
152 void ShibMySQLCCache::thread_init()
154 saml::NDC ndc("thread_init");
156 // Connect to the database
157 MYSQL* mysql = mysql_init(NULL);
159 log->error("mysql_init failed");
161 throw runtime_error("mysql_init()");
164 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
166 log->crit("mysql_real_connect failed: %s", mysql_error(mysql));
167 throw runtime_error("mysql_real_connect");
170 log->info("mysql_real_connect failed: %s. Trying to create",
173 // This will throw a runtime error if it fails.
174 createDatabase(mysql, PLUGIN_VER_MAJOR, PLUGIN_VER_MINOR);
178 int major = -1, minor = -1;
179 getVersion (mysql, &major, &minor);
181 // Make sure we've got the right version
182 if (major != PLUGIN_VER_MAJOR || minor != PLUGIN_VER_MINOR) {
184 // If we're capable, try upgrading on the fly...
185 if (major == 0 && minor == 0) {
186 upgradeDatabase(mysql);
189 log->crit("Invalid database version: %d.%d", major, minor);
190 throw runtime_error("Invalid Database version");
194 // We're all set.. Save off the handle for this thread.
195 m_mysql->setData((void*)mysql);
198 ShibMySQLCCache::ShibMySQLCCache(const DOMElement* e)
200 saml::NDC ndc("shibmysql::ShibMySQLCCache");
202 m_mysql = ThreadKey::create(&shib_mysql_destroy_handle);
203 log = &(log4cpp::Category::getInstance("shibmysql::ShibMySQLCCache"));
211 m_cache = dynamic_cast<ISessionCache*>(
212 ShibConfig::getConfig().m_plugMgr.newPlugin(
213 "edu.internet2.middleware.shibboleth.target.provider.MemoryCache", e
217 // Initialize the cleanup thread
218 shutdown_wait = CondWait::create();
220 cleanup_thread = Thread::create(&cleanup_fcn, (void*)this);
223 ShibMySQLCCache::~ShibMySQLCCache()
226 shutdown_wait->signal();
227 cleanup_thread->join(NULL);
236 ISessionCacheEntry* ShibMySQLCCache::find(const char* key)
238 saml::NDC ndc("mysql::find");
239 ISessionCacheEntry* res = m_cache->find(key);
242 log->debug("Looking in database...");
244 // nothing cached; see if this exists in the database
245 string q = string("SELECT application_id,addr,statement FROM state WHERE cookie='") + key + "' LIMIT 1";
248 MYSQL* mysql = getMYSQL();
249 if (mysql_query(mysql, q.c_str()))
250 log->error("Error searching for %s: %s", key, mysql_error(mysql));
252 rows = mysql_store_result(mysql);
254 // Nope, doesn't exist.
258 // Make sure we got 1 and only 1 rows.
259 if (mysql_num_rows(rows) != 1) {
260 log->error("Select returned wrong number of rows: %d", mysql_num_rows(rows));
261 mysql_free_result(rows);
265 log->debug("Match found. Parsing...");
267 // Pull apart the row and process the results
268 MYSQL_ROW row = mysql_fetch_row(rows);
269 IConfig* conf=ShibTargetConfig::getConfig().getINI();
271 const IApplication* application=conf->getApplication(row[0]);
273 mysql_free_result(rows);
274 throw ShibTargetException(SHIBRPC_INTERNAL_ERROR,"unable to locate application for session, deleted?");
277 istringstream str(row[2]);
278 SAMLAuthenticationStatement *s = NULL;
280 // Try to parse the AuthStatement
282 s = new SAMLAuthenticationStatement(str);
284 mysql_free_result(rows);
288 // Insert it into the memory cache
290 m_cache->insert(key, application, s, row[1]);
292 // Free the results, and then re-run the 'find' query
293 mysql_free_result(rows);
294 res = m_cache->find(key);
299 return new ShibMySQLCCacheEntry(key, res, this);
302 void ShibMySQLCCache::insert(
304 const IApplication* application,
305 saml::SAMLAuthenticationStatement *s,
306 const char *client_addr,
307 saml::SAMLResponse* r)
309 saml::NDC ndc("mysql::insert");
313 string q = string("INSERT INTO state VALUES('") + key + "','" + application->getId() + "',NOW(),'" + client_addr + "','" + os.str() + "')";
315 log->debug("Query: %s", q.c_str());
317 // Add it to the memory cache
318 m_cache->insert(key, application, s, client_addr, r);
320 // then add it to the database
321 MYSQL* mysql = getMYSQL();
322 if (mysql_query(mysql, q.c_str()))
323 log->error("Error inserting %s: %s", key, mysql_error(mysql));
326 void ShibMySQLCCache::remove(const char* key)
328 saml::NDC ndc("mysql::remove");
330 // Remove the cached version
331 m_cache->remove(key);
333 // Remove from the database
334 string q = string("DELETE FROM state WHERE cookie='") + key + "'";
335 MYSQL* mysql = getMYSQL();
336 if (mysql_query(mysql, q.c_str()))
337 log->info("Error deleting entry %s: %s", key, mysql_error(mysql));
340 void ShibMySQLCCache::cleanup()
342 Mutex* mutex = Mutex::create();
343 saml::NDC ndc("mysql::cleanup");
348 int timeout_life = 0;
350 // Load our configuration details...
351 const XMLCh* tag=m_root->getAttributeNS(NULL,cleanupInterval);
353 rerun_timer = XMLString::parseInt(tag);
355 // search for 'mysql-cache-timeout' and then the regular cache timeout
356 tag=m_root->getAttributeNS(NULL,mysqlTimeout);
358 timeout_life = XMLString::parseInt(tag);
360 tag=m_root->getAttributeNS(NULL,cacheTimeout);
362 timeout_life = XMLString::parseInt(tag);
365 if (rerun_timer <= 0)
366 rerun_timer = 300; // rerun every 5 minutes
368 if (timeout_life <= 0)
369 timeout_life = 28800; // timeout after 8 hours
373 MYSQL* mysql = getMYSQL();
375 while (shutdown == false) {
376 shutdown_wait->timedwait(mutex, rerun_timer);
378 if (shutdown == true)
381 // Find all the entries in the database that haven't been used
382 // recently In particular, find all entries that have not been
383 // accessed in 'timeout_life' seconds.
385 q << "SELECT cookie FROM state WHERE " <<
386 "UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(atime) >= " << timeout_life;
389 if (mysql_query(mysql, q.str().c_str()))
390 log->error("Error searching for old items: %s", mysql_error(mysql));
392 rows = mysql_store_result(mysql);
396 if (mysql_num_fields(rows) != 1) {
397 log->error("Wrong number of rows, 1 != %d", mysql_num_fields(rows));
398 mysql_free_result(rows);
402 // For each row, remove the entry from the database.
404 while ((row = mysql_fetch_row(rows)) != NULL)
407 mysql_free_result(rows);
410 log->debug("cleanup thread exiting...");
418 void* ShibMySQLCCache::cleanup_fcn(void* cache_p)
420 ShibMySQLCCache* cache = (ShibMySQLCCache*)cache_p;
422 // First, let's block all signals
423 Thread::mask_all_signals();
425 // Now run the cleanup process.
430 void ShibMySQLCCache::createDatabase(MYSQL* mysql, int major, int minor)
432 log->info("Creating database.");
436 ms = mysql_init(NULL);
438 log->crit("mysql_init failed");
439 throw ShibTargetException();
442 if (!mysql_real_connect(ms, NULL, NULL, NULL, NULL, 0, NULL, 0)) {
443 log->crit("cannot open DB file to create DB: %s", mysql_error(ms));
444 throw ShibTargetException();
447 if (mysql_query(ms, "CREATE DATABASE shar")) {
448 log->crit("cannot create shar database: %s", mysql_error(ms));
449 throw ShibTargetException();
452 if (!mysql_real_connect(mysql, NULL, NULL, NULL, "shar", 0, NULL, 0)) {
453 log->crit("cannot open SHAR database");
454 throw ShibTargetException();
459 } catch (ShibTargetException&) {
463 throw runtime_error("mysql_real_connect");
466 // Now create the tables if they don't exist
467 log->info("Creating database tables.");
469 if (mysql_query(mysql, "CREATE TABLE version (major INT, minor INT)"))
470 log->error ("Error creating version: %s", mysql_error(mysql));
472 if (mysql_query(mysql,
473 "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
474 "atime DATETIME, addr VARCHAR(128), statement TEXT)"))
475 log->error ("Error creating state: %s", mysql_error(mysql));
478 q << "INSERT INTO version VALUES(" << major << "," << minor << ")";
479 if (mysql_query(mysql, q.str().c_str()))
480 log->error ("Error setting version: %s", mysql_error(mysql));
483 void ShibMySQLCCache::upgradeDatabase(MYSQL* mysql)
485 if (mysql_query(mysql, "DROP TABLE state")) {
486 log->error("Error dropping old session state table: %s", mysql_error(mysql));
489 if (mysql_query(mysql,
490 "CREATE TABLE state (cookie VARCHAR(64) PRIMARY KEY, application_id VARCHAR(255),"
491 "atime DATETIME, addr VARCHAR(128), statement TEXT)")) {
492 log->error ("Error creating state table: %s", mysql_error(mysql));
493 throw runtime_error("error creating table");
497 q << "UPDATE version SET major = " << PLUGIN_VER_MAJOR;
498 if (mysql_query(mysql, q.str().c_str())) {
499 log->error ("Error updating version: %s", mysql_error(mysql));
500 throw runtime_error("error updating table");
504 void ShibMySQLCCache::getVersion(MYSQL* mysql, int* major_p, int* minor_p)
506 // grab the version number from the database
507 if (mysql_query(mysql, "SELECT * FROM version"))
508 log->error ("Error reading version: %s", mysql_error(mysql));
510 MYSQL_RES* rows = mysql_store_result(mysql);
512 if (mysql_num_rows(rows) == 1 && mysql_num_fields(rows) == 2) {
513 MYSQL_ROW row = mysql_fetch_row(rows);
515 int major = row[0] ? atoi(row[0]) : -1;
516 int minor = row[1] ? atoi(row[1]) : -1;
517 log->debug("opening database version %d.%d", major, minor);
519 mysql_free_result (rows);
526 // Wrong number of rows or wrong number of fields...
528 log->crit("Houston, we've got a problem with the database..");
529 mysql_free_result (rows);
530 throw runtime_error("Database version verification failed");
533 log->crit("MySQL Read Failed in version verificatoin");
534 throw runtime_error("MySQL Read Failed");
537 void ShibMySQLCCache::mysqlInit(void)
539 log->info ("Opening MySQL Database");
541 // Setup the argument array
542 vector<string> arg_array;
543 arg_array.push_back("shar");
545 // grab any MySQL parameters from the config file
546 const DOMElement* e=saml::XML::getFirstChildElement(m_root,ShibTargetConfig::SHIBTARGET_NS,Argument);
548 auto_ptr_char arg(e->getFirstChild()->getNodeValue());
550 arg_array.push_back(arg.get());
551 e=saml::XML::getNextSiblingElement(e,ShibTargetConfig::SHIBTARGET_NS,Argument);
554 // Compute the argument array
555 int arg_count = arg_array.size();
556 const char** args=new const char*[arg_count];
557 for (int i = 0; i < arg_count; i++)
558 args[i] = arg_array[i].c_str();
560 // Initialize MySQL with the arguments
561 mysql_server_init(arg_count, (char **)args, NULL);
566 /*************************************************************************
567 * The CCacheEntry here is mostly a wrapper around the "memory"
568 * cacheentry provided by shibboleth. The only difference is that we
569 * intercept the isSessionValid() so that we can "touch()" the
570 * database if the session is still valid.
573 ShibMySQLCCacheEntry::ShibMySQLCCacheEntry(const char* key, ISessionCacheEntry* entry, ShibMySQLCCache* cache)
575 m_cacheEntry = entry;
580 bool ShibMySQLCCacheEntry::isValid(time_t lifetime, time_t timeout) const
582 bool res = m_cacheEntry->isValid(lifetime, timeout);
588 bool ShibMySQLCCacheEntry::touch() const
590 string q=string("UPDATE state SET atime=NOW() WHERE cookie='") + m_key + "'";
592 MYSQL* mysql = m_cache->getMYSQL();
593 if (mysql_query(mysql, q.c_str())) {
594 m_cache->log->info("Error updating timestamp on %s: %s",
595 m_key.c_str(), mysql_error(mysql));
601 /*************************************************************************
602 * The registration functions here...
605 IPlugIn* new_mysql_ccache(const DOMElement* e)
607 return new ShibMySQLCCache(e);
610 extern "C" int SHIBMYSQL_EXPORTS saml_extension_init(void*)
612 // register this ccache type
613 ShibConfig::getConfig().m_plugMgr.regFactory(
614 "edu.internet2.middleware.shibboleth.target.provider.MySQLSessionCache", &new_mysql_ccache
619 /*************************************************************************
623 extern "C" void shib_mysql_destroy_handle(void* data)
625 MYSQL* mysql = (MYSQL*) data;