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