45184eb3b4eb1b1aefe36e15fa63845fb4d8998c
[shibboleth/cpp-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* getApplicationID() const {\r
78             return m_obj["application_id"].string();\r
79         }\r
80         const char* getClientAddress() const {\r
81             return m_obj["client_addr"].string();\r
82         }\r
83         const char* getEntityID() const {\r
84             return m_obj["entity_id"].string();\r
85         }\r
86         const char* getAuthnInstant() const {\r
87             return m_obj["authn_instant"].string();\r
88         }\r
89         const char* getSessionIndex() const {\r
90             return m_obj["session_index"].string();\r
91         }\r
92         const char* getAuthnContextClassRef() const {\r
93             return m_obj["authncontext_class"].string();\r
94         }\r
95         const char* getAuthnContextDeclRef() const {\r
96             return m_obj["authncontext_decl"].string();\r
97         }\r
98         const multimap<string,Attribute*>& getAttributes() const {\r
99             if (m_attributes.empty())\r
100                 unmarshallAttributes();\r
101             return m_attributes;\r
102         }\r
103         const vector<const char*>& getAssertionIDs() const {\r
104             if (m_ids.empty()) {\r
105                 DDF ids = m_obj["assertions"];\r
106                 DDF id = ids.first();\r
107                 while (id.isstring()) {\r
108                     m_ids.push_back(id.string());\r
109                     id = ids.next();\r
110                 }\r
111             }\r
112             return m_ids;\r
113         }\r
114         \r
115         time_t expires() const { return m_expires; }\r
116         time_t lastAccess() const { return m_lastAccess; }\r
117         void validate(const Application& application, const char* client_addr, time_t timeout, bool local=true);\r
118 \r
119     private:\r
120         void unmarshallAttributes() const;\r
121 \r
122         int m_version;\r
123         mutable DDF m_obj;\r
124         mutable multimap<string,Attribute*> m_attributes;\r
125         mutable vector<const char*> m_ids;\r
126         time_t m_expires,m_lastAccess;\r
127         RemotedCache* m_cache;\r
128         Mutex* m_lock;\r
129     };\r
130     \r
131     class RemotedCache : public SessionCache\r
132     {\r
133     public:\r
134         RemotedCache(const DOMElement* e);\r
135         ~RemotedCache();\r
136     \r
137         Session* find(const char* key, const Application& application, const char* client_addr=NULL, time_t timeout=0);\r
138         void remove(const char* key, const Application& application, const char* client_addr);\r
139         \r
140         void cleanup();\r
141     \r
142         Category& m_log;\r
143     private:\r
144         const DOMElement* m_root;         // Only valid during initialization\r
145         RWLock* m_lock;\r
146         map<string,RemotedSession*> m_hashtable;\r
147     \r
148         void dormant(const char* key);\r
149         static void* cleanup_fn(void*);\r
150         bool shutdown;\r
151         CondWait* shutdown_wait;\r
152         Thread* cleanup_thread;\r
153     };\r
154 \r
155     SessionCache* SHIBSP_DLLLOCAL RemotedCacheFactory(const DOMElement* const & e)\r
156     {\r
157         return new RemotedCache(e);\r
158     }\r
159 }\r
160 \r
161 void RemotedSession::unmarshallAttributes() const\r
162 {\r
163     Attribute* attribute;\r
164     DDF attrs = m_obj["attributes"];\r
165     DDF attr = attrs.first();\r
166     while (!attr.isnull()) {\r
167         try {\r
168             attribute = Attribute::unmarshall(attr);\r
169             m_attributes.insert(make_pair(attribute->getId(),attribute));\r
170             if (m_cache->m_log.isDebugEnabled())\r
171                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",\r
172                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");\r
173         }\r
174         catch (AttributeException& ex) {\r
175             const char* id = attr.first().name();\r
176             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());\r
177         }\r
178         attr = attrs.next();\r
179     }\r
180 }\r
181 \r
182 void RemotedSession::validate(const Application& application, const char* client_addr, time_t timeout, bool local)\r
183 {\r
184     // Basic expiration?\r
185     time_t now = time(NULL);\r
186     if (now > m_expires) {\r
187         m_cache->m_log.info("session expired (ID: %s)", m_obj.name());\r
188         throw opensaml::RetryableProfileException("Your session has expired, and you must re-authenticate.");\r
189     }\r
190 \r
191     // Address check?\r
192     if (client_addr) {\r
193         if (m_cache->m_log.isDebugEnabled())\r
194             m_cache->m_log.debug("comparing client address %s against %s", client_addr, getClientAddress());\r
195         if (strcmp(getClientAddress(),client_addr)) {\r
196             m_cache->m_log.warn("client address mismatch");\r
197             throw opensaml::RetryableProfileException(\r
198                 "Your IP address ($1) does not match the address recorded at the time the session was established.",\r
199                 params(1,client_addr)\r
200                 );\r
201         }\r
202     }\r
203 \r
204     if (local)\r
205         return;\r
206     \r
207     DDF in("touch::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
208     DDFJanitor jin(in);\r
209     in.structure();\r
210     in.addmember("key").string(m_obj.name());\r
211     in.addmember("version").integer(m_obj["version"].integer());\r
212     if (timeout) {\r
213         // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
214 #ifndef HAVE_GMTIME_R\r
215         struct tm* ptime=gmtime(&timeout);\r
216 #else\r
217         struct tm res;\r
218         struct tm* ptime=gmtime_r(&timeout,&res);\r
219 #endif\r
220         char timebuf[32];\r
221         strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
222         in.addmember("timeout").string(timebuf);\r
223     }\r
224 \r
225     try {\r
226         out=application.getServiceProvider().getListenerService()->send(in);\r
227     }\r
228     catch (...) {\r
229         out.destroy();\r
230         throw;\r
231     }\r
232 \r
233     if (out.isstruct()) {\r
234         // We got an updated record back.\r
235         m_ids.clear();\r
236         for_each(m_attributes.begin(), m_attributes.end(), cleanup_const_pair<string,Attribute>());\r
237         m_attributes.clear();\r
238         m_obj.destroy();\r
239         m_obj = out;\r
240     }\r
241 \r
242     m_lastAccess = now;\r
243 }\r
244 \r
245 RemotedCache::RemotedCache(const DOMElement* e)\r
246     : SessionCache(e), m_log(Category::getInstance(SHIBSP_LOGCAT".SessionCache")), m_root(e), m_lock(NULL), shutdown(false)\r
247 {\r
248     if (!SPConfig::getConfig().getServiceProvider()->getListenerService())\r
249         throw ConfigurationException("RemotedCacheService requires a ListenerService, but none available.");\r
250         \r
251     m_lock = RWLock::create();\r
252     shutdown_wait = CondWait::create();\r
253     cleanup_thread = Thread::create(&cleanup_fn, (void*)this);\r
254 }\r
255 \r
256 RemotedCache::~RemotedCache()\r
257 {\r
258     // Shut down the cleanup thread and let it know...\r
259     shutdown = true;\r
260     shutdown_wait->signal();\r
261     cleanup_thread->join(NULL);\r
262 \r
263     for_each(m_hashtable.begin(),m_hashtable.end(),xmltooling::cleanup_pair<string,RemotedSession>());\r
264     delete m_lock;\r
265     delete shutdown_wait;\r
266 }\r
267 \r
268 Session* RemotedCache::find(const char* key, const Application& application, const char* client_addr, time_t timeout)\r
269 {\r
270 #ifdef _DEBUG\r
271     xmltooling::NDC ndc("find");\r
272 #endif\r
273 \r
274     bool localValidation = false;\r
275     RemotedSession* session=NULL;\r
276     m_log.debug("searching local cache for session (%s)", key);\r
277     m_lock->rdlock();\r
278     map<string,RemotedSession*>::const_iterator i=m_hashtable.find(key);\r
279     if (i==m_hashtable.end()) {\r
280         m_lock->unlock();\r
281         m_log.debug("session not found locally, searching remote cache");\r
282 \r
283         DDF in("find::"REMOTED_SESSION_CACHE"::SessionCache"), out;\r
284         DDFJanitor jin(in);\r
285         in.structure();\r
286         in.addmember("key").string(key);\r
287         if (timeout) {\r
288             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.  \r
289 #ifndef HAVE_GMTIME_R\r
290             struct tm* ptime=gmtime(&timeout);\r
291 #else\r
292             struct tm res;\r
293             struct tm* ptime=gmtime_r(&timeout,&res);\r
294 #endif\r
295             char timebuf[32];\r
296             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);\r
297             in.addmember("timeout").string(timebuf);\r
298         }\r
299         \r
300         try {\r
301             out=application.getServiceProvider().getListenerService()->send(in);\r
302             if (!out.isstruct()) {\r
303                 out.destroy();\r
304                 m_log.debug("session not found in remote cache");\r
305                 return NULL;\r
306             }\r
307             \r
308             // Wrap the results in a local entry and save it.\r
309             session = new RemotedSession(this, out);\r
310             // The remote end has handled timeout issues, we handle address and expiration checks.\r
311             localValidation = true;\r
312         }\r
313         catch (...) {\r
314             out.destroy();\r
315             throw;\r
316         }\r
317 \r
318         // Lock for writing and repeat the search to avoid duplication.\r
319         m_lock->wrlock();\r
320         SharedLock shared(m_lock, false);\r
321         if (m_hashtable.count(key)) {\r
322             delete session;\r
323             // We're using an existing session entry, so we have to switch back to full validation.\r
324             localValidation = false;\r
325             session = m_hashtable[key];\r
326             session->lock();\r
327         }\r
328         else {\r
329             m_hashtable[key]=session;\r
330             session->lock();\r
331         }\r
332     }\r
333     else {\r
334         // Save off and lock the session.\r
335         session = i->second;\r
336         session->lock();\r
337         m_lock->unlock();\r
338         \r
339         m_log.debug("session found locally, validating it for use");\r
340     }\r
341 \r
342     if (!XMLString::equals(session->getApplicationID(), application.getId())) {\r
343         m_log.error("an application (%s) tried to access another application's session", application.getId());\r
344         session->unlock();\r
345         return NULL;\r
346     }\r
347 \r
348     // Verify currency and update the timestamp.\r
349     // If the local switch is false, we also update the access time.\r
350     try {\r
351         session->validate(application, client_addr, timeout, localValidation);\r
352     }\r
353     catch (...) {\r
354         session->unlock();\r
355         remove(key, application, client_addr);\r
356         throw;\r
357     }\r
358     \r
359     return session;\r
360 }\r
361 \r
362 void RemotedCache::remove(const char* key, const Application& application, const char* client_addr)\r
363 {\r
364     // Take care of local copy.\r
365     dormant(key);\r
366     \r
367     // Now remote...\r
368     DDF in("remove::"REMOTED_SESSION_CACHE"::SessionCache");\r
369     DDFJanitor jin(in);\r
370     in.structure();\r
371     in.addmember("key").string(key);\r
372     in.addmember("application_id").string(application.getId());\r
373     in.addmember("client_addr").string(client_addr);\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