f9162b7a5812192b62c32b0c113175c500a02803
[shibboleth/sp.git] / shibsp / impl / RemotedSessionCache.cpp
1 /*\r
2  *  Copyright 2001-2007 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /**\r
18  * RemotedSessionCache.cpp\r
19  * \r
20  * SessionCache implementation that delegates to a remoted version.\r
21  */\r
22 \r
23 #include "internal.h"\r
24 #include "Application.h"\r
25 #include "exceptions.h"\r
26 #include "ServiceProvider.h"\r
27 #include "SessionCache.h"\r
28 #include "attribute/Attribute.h"\r
29 #include "remoting/ListenerService.h"\r
30 #include "util/SPConstants.h"\r
31 \r
32 #include <ctime>\r
33 #include <sstream>\r
34 #include <xmltooling/XMLToolingConfig.h>\r
35 #include <xmltooling/util/DateTime.h>\r
36 #include <xmltooling/util/NDC.h>\r
37 #include <xmltooling/util/XMLHelper.h>\r
38 \r
39 using namespace shibsp;\r
40 using namespace xmltooling;\r
41 using namespace std;\r
42 \r
43 namespace shibsp {\r
44 \r
45     class RemotedCache;\r
46     class RemotedSession : public virtual Session\r
47     {\r
48     public:\r
49         RemotedSession(RemotedCache* cache, DDF& obj) : m_version(obj["version"].integer()), m_obj(obj),\r
50                 m_expires(0), m_lastAccess(time(NULL)), m_cache(cache), m_lock(NULL) {\r
51             auto_ptr_XMLCh exp(m_obj["expires"].string());\r
52             if (exp.get()) {\r
53                 DateTime iso(exp.get());\r
54                 iso.parseDateTime();\r
55                 m_expires = iso.getEpoch();\r
56             }\r
57 \r
58             m_lock = Mutex::create();\r
59         }\r
60         \r
61         ~RemotedSession() {\r
62             delete m_lock;\r
63             m_obj.destroy();\r
64             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
65         }\r
66         \r
67         Lockable* lock() {\r
68             m_lock->lock();\r
69             return this;\r
70         }\r
71         void unlock() {\r
72             m_lock->unlock();\r
73         }\r
74 \r
75         const char* getID() const {\r
76             return m_obj.name();\r
77         }\r
78         const char* getApplicationID() const {\r
79             return m_obj["application_id"].string();\r
80         }\r
81         const char* getClientAddress() const {\r
82             return m_obj["client_addr"].string();\r
83         }\r
84         const char* getEntityID() const {\r
85             return m_obj["entity_id"].string();\r
86         }\r
87         const char* getProtocol() const {\r
88             return m_obj["protocol"].string();\r
89         }\r
90         const char* getAuthnInstant() const {\r
91             return m_obj["authn_instant"].string();\r
92         }\r
93         const char* getSessionIndex() const {\r
94             return m_obj["session_index"].string();\r
95         }\r
96         const char* getAuthnContextClassRef() const {\r
97             return m_obj["authncontext_class"].string();\r
98         }\r
99         const char* getAuthnContextDeclRef() const {\r
100             return m_obj["authncontext_decl"].string();\r
101         }\r
102         const vector<Attribute*>& getAttributes() const {\r
103             if (m_attributes.empty())\r
104                 unmarshallAttributes();\r
105             return m_attributes;\r
106         }\r
107         const multimap<string,const Attribute*>& getIndexedAttributes() const {\r
108             if (m_attributeIndex.empty()) {\r
109                 if (m_attributes.empty())\r
110                     unmarshallAttributes();\r
111                 for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {\r
112                     const vector<string>& aliases = (*a)->getAliases();\r
113                     for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)\r
114                         m_attributeIndex.insert(make_pair(*alias, *a));\r
115                 }\r
116             }\r
117             return m_attributeIndex;\r
118         }\r
119         const vector<const char*>& getAssertionIDs() const {\r
120             if (m_ids.empty()) {\r
121                 DDF ids = m_obj["assertions"];\r
122                 DDF id = ids.first();\r
123                 while (id.isstring()) {\r
124                     m_ids.push_back(id.string());\r
125                     id = ids.next();\r
126                 }\r
127             }\r
128             return m_ids;\r
129         }\r
130         \r
131         time_t expires() const { return m_expires; }\r
132         time_t lastAccess() const { return m_lastAccess; }\r
133         void validate(const Application& application, const char* client_addr, time_t* timeout);\r
134 \r
135     private:\r
136         void unmarshallAttributes() const;\r
137 \r
138         int m_version;\r
139         mutable DDF m_obj;\r
140         mutable vector<Attribute*> m_attributes;\r
141         mutable multimap<string,const Attribute*> m_attributeIndex;\r
142         mutable vector<const char*> m_ids;\r
143         time_t m_expires,m_lastAccess;\r
144         RemotedCache* m_cache;\r
145         Mutex* m_lock;\r
146     };\r
147     \r
148     class RemotedCache : public SessionCache\r
149     {\r
150     public:\r
151         RemotedCache(const DOMElement* e);\r
152         ~RemotedCache();\r
153     \r
154         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t* timeout=NULL);\r
155         void remove(const char* key, const Application& application);\r
156         \r
157         void cleanup();\r
158     \r
159         Category& m_log;\r
160     private:\r
161         const DOMElement* m_root;         // Only valid during initialization\r
162         RWLock* m_lock;\r
163         map<string,RemotedSession*> m_hashtable;\r
164     \r
165         void dormant(const char* key);\r
166         static void* cleanup_fn(void*);\r
167         bool shutdown;\r
168         CondWait* shutdown_wait;\r
169         Thread* cleanup_thread;\r
170     };\r
171 \r
172     SessionCache* SHIBSP_DLLLOCAL RemotedCacheFactory(const DOMElement* const & e)\r
173     {\r
174         return new RemotedCache(e);\r
175     }\r
176 }\r
177 \r
178 void RemotedSession::unmarshallAttributes() const\r
179 {\r
180     Attribute* attribute;\r
181     DDF attrs = m_obj["attributes"];\r
182     DDF attr = attrs.first();\r
183     while (!attr.isnull()) {\r
184         try {\r
185             attribute = Attribute::unmarshall(attr);\r
186             m_attributes.push_back(attribute);\r
187             if (m_cache->m_log.isDebugEnabled())\r
188                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",\r
189                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");\r
190         }\r
191         catch (AttributeException& ex) {\r
192             const char* id = attr.first().name();\r
193             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());\r
194         }\r
195         attr = attrs.next();\r
196     }\r
197 }\r
198 \r
199 void RemotedSession::validate(const Application& application, const char* client_addr, time_t* timeout)\r
200 {\r
201     // Basic expiration?\r
202     time_t now = time(NULL);\r
203     if (now > m_expires) {\r
204         m_cache->m_log.info("session expired (ID: %s)", getID());\r
205         throw opensaml::RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
206     }\r
207 \r
208     // Address check?\r
209     if (client_addr) {\r
210         if (m_cache->m_log.isDebugEnabled())\r
211             m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());\r
212         if (strcmp(getClientAddress(),client_addr)) {\r
213             m_cache->m_log.warn("client address mismatch");\r
214             throw opensaml::RetryableProfileException(\r
215                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
216                 params(1,client_addr)\r
217                 );\r
218         }\r
219     }\r
220 \r
221     if (!timeout)\r
222         return;\r
223     \r
224     DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
225     DDFJanitor jin(in);\r
226     in.structure();\r
227     in.addmember("key").string(getID());\r
228     in.addmember("version").integer(m_obj["version"].integer());\r
229     if (*timeout) {\r
230         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
231 #ifndef HAVE_GMTIME_R\r
232         struct tm* ptime=gmtime(timeout);\r
233 #else\r
234         struct tm res;\r
235         struct tm* ptime=gmtime_r(timeout,&res);\r
236 #endif\r
237         char timebuf[32];\r
238         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
239         in.addmember("timeout").string(timebuf);\r
240     }\r
241 \r
242     try {\r
243         out=application.getServiceProvider().getListenerService()->send(in);\r
244     }\r
245     catch (...) {\r
246         out.destroy();\r
247         throw;\r
248     }\r
249 \r
250     if (out.isstruct()) {\r
251         // We got an updated record back.\r
252         m_ids.clear();\r
253         for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());\r
254         m_attributes.clear();\r
255         m_attributeIndex.clear();\r
256         m_obj.destroy();\r
257         m_obj = out;\r
258     }\r
259 \r
260     m_lastAccess = now;\r
261 }\r
262 \r
263 RemotedCache::RemotedCache(const DOMElement* e)\r
264     : SessionCache(e, 900), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_root(e), m_lock(NULL), shutdown(false)\r
265 {\r
266     if (!SPConfig::getConfig().getServiceProvider()->getListenerService())\r
267         throw ConfigurationException("RemotedCacheService requires a ListenerService, but none available.");\r
268         \r
269     m_lock = RWLock::create();\r
270     shutdown_wait = CondWait::create();\r
271     cleanup_thread = Thread::create(&cleanup_fn, (void*)this);\r
272 }\r
273 \r
274 RemotedCache::~RemotedCache()\r
275 {\r
276     // Shut down the cleanup thread and let it know...\r
277     shutdown = true;\r
278     shutdown_wait->signal();\r
279     cleanup_thread->join(NULL);\r
280 \r
281     for_each(m_hashtable.begin(),m_hashtable.end(),xmltooling::cleanup_pair<string,RemotedSession>());\r
282     delete m_lock;\r
283     delete shutdown_wait;\r
284 }\r
285 \r
286 Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t* timeout)\r
287 {\r
288 #ifdef _DEBUG\r
289     xmltooling::NDC ndc("find");\r
290 #endif\r
291 \r
292     RemotedSession* session=NULL;\r
293     m_log.debug("searching local cache for session (%s)", key);\r
294     m_lock->rdlock();\r
295     map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);\r
296     if (i==m_hashtable.end()) {\r
297         m_lock->unlock();\r
298         m_log.debug("session not found locally, searching remote cache");\r
299 \r
300         DDF in("find::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
301         DDFJanitor jin(in);\r
302         in.structure();\r
303         in.addmember("key").string(key);\r
304         in.addmember("application_id").string(application.getId());\r
305         if (timeout && *timeout) {\r
306             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
307 #ifndef HAVE_GMTIME_R\r
308             struct tm* ptime=gmtime(timeout);\r
309 #else\r
310             struct tm res;\r
311             struct tm* ptime=gmtime_r(timeout,&res);\r
312 #endif\r
313             char timebuf[32];\r
314             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
315             in.addmember("timeout").string(timebuf);\r
316         }\r
317         \r
318         try {\r
319             out=application.getServiceProvider().getListenerService()->send(in);\r
320             if (!out.isstruct()) {\r
321                 out.destroy();\r
322                 m_log.debug("session not found in remote cache");\r
323                 return NULL;\r
324             }\r
325             \r
326             // Wrap the results in a local entry and save it.\r
327             session = new RemotedSession(this, out);\r
328             // The remote end has handled timeout issues, we handle address and expiration checks.\r
329             timeout = NULL;\r
330         }\r
331         catch (...) {\r
332             out.destroy();\r
333             throw;\r
334         }\r
335 \r
336         // Lock for writing and repeat the search to avoid duplication.\r
337         m_lock->wrlock();\r
338         SharedLock shared(m_lock, false);\r
339         if (m_hashtable.count(key)) {\r
340             // We're using an existing session entry.\r
341             delete session;\r
342             session = m_hashtable[key];\r
343             session->lock();\r
344         }\r
345         else {\r
346             m_hashtable[key]=session;\r
347             session->lock();\r
348         }\r
349     }\r
350     else {\r
351         // Save off and lock the session.\r
352         session = i->second;\r
353         session->lock();\r
354         m_lock->unlock();\r
355         \r
356         m_log.debug("session found locally, validating it for use");\r
357     }\r
358 \r
359     if (!XMLString::equals(session->getApplicationID(), application.getId())) {\r
360         m_log.error("an application (%s) tried to access another application's session", application.getId());\r
361         session->unlock();\r
362         return NULL;\r
363     }\r
364 \r
365     // Verify currency and update the timestamp if indicated by caller.\r
366     try {\r
367         session->validate(application, client_addr, timeout);\r
368     }\r
369     catch (...) {\r
370         session->unlock();\r
371         remove(key, application);\r
372         throw;\r
373     }\r
374     \r
375     return session;\r
376 }\r
377 \r
378 void RemotedCache::remove(const char* key, const Application& application)\r
379 {\r
380     // Take care of local copy.\r
381     dormant(key);\r
382     \r
383     // Now remote...\r
384     DDF in("remove::"REMOTED_SESSION_CACHE"::SessionCache");\r
385     DDFJanitor jin(in);\r
386     in.structure();\r
387     in.addmember("key").string(key);\r
388     in.addmember("application_id").string(application.getId());\r
389     \r
390     DDF out = application.getServiceProvider().getListenerService()->send(in);\r
391     out.destroy();\r
392 }\r
393 \r
394 void RemotedCache::dormant(const char* key)\r
395 {\r
396 #ifdef _DEBUG\r
397     xmltooling::NDC ndc("dormant");\r
398 #endif\r
399 \r
400     m_log.debug("deleting local copy of session (%s)", key);\r
401 \r
402     // lock the cache for writing, which means we know nobody is sitting in find()\r
403     m_lock->wrlock();\r
404 \r
405     // grab the entry from the table\r
406     map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);\r
407     if (i==m_hashtable.end()) {\r
408         m_lock->unlock();\r
409         return;\r
410     }\r
411 \r
412     // ok, remove the entry and lock it\r
413     RemotedSession* entry=i->second;\r
414     m_hashtable.erase(key);\r
415     entry->lock();\r
416     \r
417     // unlock the cache\r
418     m_lock->unlock();\r
419 \r
420     // we can release the cache entry lock because we know we're not in the cache anymore\r
421     entry->unlock();\r
422 \r
423     delete entry;\r
424 }\r
425 \r
426 void RemotedCache::cleanup()\r
427 {\r
428 #ifdef _DEBUG\r
429     xmltooling::NDC ndc("cleanup");\r
430 #endif\r
431 \r
432     Mutex* mutex = Mutex::create();\r
433   \r
434     // Load our configuration details...\r
435     static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);\r
436     const XMLCh* tag=m_root ? m_root->getAttributeNS(NULL,cleanupInterval) : NULL;\r
437     int rerun_timer = 900;\r
438     if (tag && *tag)\r
439         rerun_timer = XMLString::parseInt(tag);\r
440     if (rerun_timer <= 0)\r
441         rerun_timer = 900;\r
442 \r
443     mutex->lock();\r
444 \r
445     m_log.info("cleanup thread started...run every %d secs; timeout after %d secs", rerun_timer, m_cacheTimeout);\r
446 \r
447     while (!shutdown) {\r
448         shutdown_wait->timedwait(mutex,rerun_timer);\r
449         if (shutdown)\r
450             break;\r
451 \r
452         // Ok, let's run through the cleanup process and clean out\r
453         // really old sessions.  This is a two-pass process.  The\r
454         // first pass is done holding a read-lock while we iterate over\r
455         // the cache.  The second pass doesn't need a lock because\r
456         // the 'deletes' will lock the cache.\r
457     \r
458         // Pass 1: iterate over the map and find all entries that have not been\r
459         // used in X hours\r
460         vector<string> stale_keys;\r
461         time_t stale = time(NULL) - m_cacheTimeout;\r
462     \r
463         m_log.debug("cleanup thread running");\r
464 \r
465         m_lock->rdlock();\r
466         for (map<string,RemotedSession*>::const_iterator i=m_hashtable.begin(); i!=m_hashtable.end(); ++i) {\r
467             // If the last access was BEFORE the stale timeout...\r
468             i->second->lock();\r
469             time_t last=i->second->lastAccess();\r
470             i->second->unlock();\r
471             if (last < stale)\r
472                 stale_keys.push_back(i->first);\r
473         }\r
474         m_lock->unlock();\r
475     \r
476         if (!stale_keys.empty()) {\r
477             m_log.info("purging %d old sessions", stale_keys.size());\r
478     \r
479             // Pass 2: walk through the list of stale entries and remove them from the cache\r
480             for (vector<string>::const_iterator j = stale_keys.begin(); j != stale_keys.end(); ++j)\r
481                 dormant(j->c_str());\r
482         }\r
483 \r
484         m_log.debug("cleanup thread completed");\r
485     }\r
486 \r
487     m_log.info("cleanup thread exiting");\r
488 \r
489     mutex->unlock();\r
490     delete mutex;\r
491     Thread::exit(NULL);\r
492 }\r
493 \r
494 void* RemotedCache::cleanup_fn(void* cache_p)\r
495 {\r
496     RemotedCache* cache = reinterpret_cast<RemotedCache*>(cache_p);\r
497 \r
498 #ifndef WIN32\r
499     // First, let's block all signals \r
500     Thread::mask_all_signals();\r
501 #endif\r
502 \r
503     // Now run the cleanup process.\r
504     cache->cleanup();\r
505     return NULL;\r
506 }\r