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