1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20 
21 /**
22  * StoredSession.cpp
23  *
24  * Implementation of Session subclass used by StorageService-backed SessionCache.
25  */
26 
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "ServiceProvider.h"
30 #include "attribute/Attribute.h"
31 #include "impl/StoredSession.h"
32 #include "impl/StorageServiceSessionCache.h"
33 
34 #include <xmltooling/util/NDC.h>
35 #include <xmltooling/util/Threads.h>
36 
37 #ifndef SHIBSP_LITE
38 # include <saml/exceptions.h>
39 # include <saml/saml2/core/Assertions.h>
40 # include <xmltooling/XMLToolingConfig.h>
41 # include <xmltooling/util/ParserPool.h>
42 # include <xmltooling/util/StorageService.h>
43 # include <xercesc/util/XMLStringTokenizer.hpp>
44 using namespace opensaml::saml2md;
45 #else
46 # include <xercesc/util/XMLDateTime.hpp>
47 #endif
48 
49 using namespace shibsp;
50 using namespace opensaml;
51 using namespace xmltooling;
52 using namespace boost;
53 using namespace std;
54 
Session()55 Session::Session()
56 {
57 }
58 
~Session()59 Session::~Session()
60 {
61 }
62 
getAddressFamily(const char * addr)63 const char* StoredSession::getAddressFamily(const char* addr) {
64     if (strchr(addr, ':'))
65         return "6";
66     else
67         return "4";
68 }
69 
StoredSession(SSCache * cache,DDF & obj)70 StoredSession::StoredSession(SSCache* cache, DDF& obj)
71     : m_obj(obj), m_cache(cache), m_expires(0), m_lastAccess(time(nullptr))
72 {
73     // Check for old address format.
74     if (m_obj["client_addr"].isstring()) {
75         const char* saddr = m_obj["client_addr"].string();
76         DDF addrobj = m_obj["client_addr"].structure();
77         if (saddr && *saddr) {
78             addrobj.addmember(getAddressFamily(saddr)).string(saddr);
79         }
80     }
81 
82     auto_ptr_XMLCh exp(m_obj["expires"].string());
83     if (exp.get()) {
84         XMLDateTime iso(exp.get());
85         iso.parseDateTime();
86         m_expires = iso.getEpoch();
87     }
88 
89 #ifndef SHIBSP_LITE
90     const char* nameid = obj["nameid"].string();
91     if (nameid) {
92         // Parse and bind the document into an XMLObject.
93         istringstream instr(nameid);
94         DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
95         XercesJanitor<DOMDocument> janitor(doc);
96         m_nameid.reset(saml2::NameIDBuilder::buildNameID());
97         m_nameid->unmarshall(doc->getDocumentElement(), true);
98         janitor.release();
99     }
100 #endif
101     if (cache->inproc)
102         m_lock.reset(Mutex::create());
103 }
104 
~StoredSession()105 StoredSession::~StoredSession()
106 {
107     m_obj.destroy();
108     for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
109 }
110 
lock()111 Lockable* StoredSession::lock()
112 {
113     if (m_lock.get())
114         m_lock->lock();
115     return this;
116 }
unlock()117 void StoredSession::unlock()
118 {
119     if (m_lock.get())
120         m_lock->unlock();
121     else
122         delete this;
123 }
124 
getIndexedAttributes() const125 const multimap<string, const Attribute*>& StoredSession::getIndexedAttributes() const
126 {
127     if (m_attributeIndex.empty()) {
128         if (m_attributes.empty())
129             unmarshallAttributes();
130         for (vector<Attribute*>::const_iterator a = m_attributes.begin(); a != m_attributes.end(); ++a) {
131             const vector<string>& aliases = (*a)->getAliases();
132             for (vector<string>::const_iterator alias = aliases.begin(); alias != aliases.end(); ++alias)
133                 m_attributeIndex.insert(multimap<string, const Attribute*>::value_type(*alias, *a));
134         }
135     }
136     return m_attributeIndex;
137 }
138 
getAssertionIDs() const139 const vector<const char*>& StoredSession::getAssertionIDs() const
140 {
141     if (m_ids.empty()) {
142         DDF ids = m_obj["assertions"];
143         DDF id = ids.first();
144         while (id.isstring()) {
145             m_ids.push_back(id.string());
146             id = ids.next();
147         }
148     }
149     return m_ids;
150 }
151 
unmarshallAttributes() const152 void StoredSession::unmarshallAttributes() const
153 {
154     Attribute* attribute;
155     DDF attrs = m_obj["attributes"];
156     DDF attr = attrs.first();
157     while (!attr.isnull()) {
158         try {
159             attribute = Attribute::unmarshall(attr);
160             m_attributes.push_back(attribute);
161             if (m_cache->m_log.isDebugEnabled())
162                 m_cache->m_log.debug("unmarshalled attribute (ID: %s) with %d value%s",
163                     attribute->getId(), attr.first().integer(), attr.first().integer()!=1 ? "s" : "");
164         }
165         catch (AttributeException& ex) {
166             const char* id = attr.first().name();
167             m_cache->m_log.error("error unmarshalling attribute (ID: %s): %s", id ? id : "none", ex.what());
168         }
169         attr = attrs.next();
170     }
171 }
172 
validate(const Application & app,const char * client_addr,time_t * timeout)173 void StoredSession::validate(const Application& app, const char* client_addr, time_t* timeout)
174 {
175     time_t now = time(nullptr);
176 
177     // Basic expiration?
178     if (m_expires > 0) {
179         if (now > m_expires) {
180             m_cache->m_log.info("session expired (ID: %s)", getID());
181             throw RetryableProfileException("Your session has expired, and you must re-authenticate.");
182         }
183     }
184 
185     // Address check?
186     if (client_addr) {
187         const char* saddr = getClientAddress(getAddressFamily(client_addr));
188         if (saddr && *saddr) {
189             if (!m_cache->compareAddresses(client_addr, saddr)) {
190                 m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
191                 throw RetryableProfileException(
192                     "Your IP address ($1) does not match the address recorded at the time the session was established.",
193                     params(1, client_addr)
194                     );
195             }
196             client_addr = nullptr;  // clear out parameter as signal that session need not be updated below
197         }
198         else {
199             m_cache->m_log.info("session (%s) not yet bound to client address type, binding it to (%s)", getID(), client_addr);
200         }
201     }
202 
203     if (!timeout && !client_addr)
204         return;
205 
206     if (!SPConfig::getConfig().isEnabled(SPConfig::OutOfProcess)) {
207         DDF in("touch::" STORAGESERVICE_SESSION_CACHE "::SessionCache"), out;
208         DDFJanitor jin(in);
209         in.structure();
210         in.addmember("key").string(getID());
211         in.addmember("version").integer(m_obj["version"].integer());
212         in.addmember("application_id").string(app.getId());
213         if (client_addr)    // signals we need to bind an additional address to the session
214             in.addmember("client_addr").string(client_addr);
215         if (timeout && *timeout) {
216             // On 64-bit Windows, time_t doesn't fit in a long, so I'm using ISO timestamps.
217 #ifndef HAVE_GMTIME_R
218             struct tm* ptime = gmtime(timeout);
219 #else
220             struct tm res;
221             struct tm* ptime = gmtime_r(timeout,&res);
222 #endif
223             char timebuf[32];
224             strftime(timebuf,32,"%Y-%m-%dT%H:%M:%SZ",ptime);
225             in.addmember("timeout").string(timebuf);
226         }
227 
228         out = app.getServiceProvider().getListenerService()->send(in);
229         if (out.isstruct()) {
230             // We got an updated record back.
231             m_cache->m_log.debug("session updated, reconstituting it");
232             m_ids.clear();
233             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
234             m_attributes.clear();
235             m_attributeIndex.clear();
236             m_obj.destroy();
237             m_obj = out;
238         }
239         else {
240             out.destroy();
241         }
242     }
243     else {
244 #ifndef SHIBSP_LITE
245         if (!m_cache->m_storage)
246             throw ConfigurationException("Session touch requires a StorageService.");
247 
248         // Versioned read, since we already have the data in hand if it's current.
249         string record;
250         time_t lastAccess = 0;
251         int curver = m_obj["version"].integer();
252         int ver = m_cache->m_storage->readText(getID(), "session", &record, &lastAccess, curver);
253         if (ver == 0) {
254             m_cache->m_log.info("session (ID: %s) no longer in storage", getID());
255             throw RetryableProfileException("Your session is not available in the session store, and you must re-authenticate.");
256         }
257 
258         if (timeout) {
259             if (lastAccess == 0) {
260                 m_cache->m_log.error("session (ID: %s) did not report time of last access", getID());
261                 throw RetryableProfileException("Your session's last access time was missing, and you must re-authenticate.");
262             }
263             // Adjust for expiration to recover last access time and check timeout.
264             unsigned long cacheTimeout = m_cache->getCacheTimeout(app);
265             lastAccess -= cacheTimeout;
266             if (*timeout > 0 && now - lastAccess >= *timeout) {
267                 m_cache->m_log.info("session timed out (ID: %s)", getID());
268                 throw RetryableProfileException("Your session has timed out due to inactivity, and you must re-authenticate.");
269             }
270 
271             // Update storage expiration, if possible.
272             try {
273                 m_cache->m_storage->updateContext(getID(), now + cacheTimeout);
274             }
275             catch (std::exception& ex) {
276                 m_cache->m_log.error("failed to update session expiration: %s", ex.what());
277             }
278         }
279 
280         if (ver > curver) {
281             // We got an updated record back.
282             DDF newobj;
283             istringstream in(record);
284             in >> newobj;
285             m_ids.clear();
286             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
287             m_attributes.clear();
288             m_attributeIndex.clear();
289             m_obj.destroy();
290             m_obj = newobj;
291         }
292 
293         // We may need to write back a new address into the session using a versioned update loop.
294         if (client_addr) {
295             short attempts = 0;
296             do {
297                 const char* saddr = getClientAddress(getAddressFamily(client_addr));
298                 if (saddr) {
299                     // Something snuck in and bound the session to this address type, so it better match what we have.
300                     if (!m_cache->compareAddresses(client_addr, saddr)) {
301                         m_cache->m_log.warn("client address mismatch, client (%s), session (%s)", client_addr, saddr);
302                         throw RetryableProfileException(
303                             "Your IP address ($1) does not match the address recorded at the time the session was established.",
304                             params(1, client_addr)
305                             );
306                     }
307                     break;  // No need to update.
308                 }
309                 else {
310                     // Bind it into the session.
311                     setClientAddress(client_addr);
312                 }
313 
314                 // Tentatively increment the version.
315                 m_obj["version"].integer(m_obj["version"].integer() + 1);
316 
317                 ostringstream str;
318                 str << m_obj;
319                 record = str.str();
320 
321                 try {
322                     ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer() - 1);
323                 }
324                 catch (std::exception&) {
325                     m_obj["version"].integer(m_obj["version"].integer() - 1);
326                     throw;
327                 }
328 
329                 if (ver <= 0) {
330                     m_obj["version"].integer(m_obj["version"].integer() - 1);
331                 }
332 
333                 if (!ver) {
334                     // Fatal problem with update.
335                     m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
336                     throw IOException("Unable to update stored session.");
337                 }
338                 else if (ver < 0) {
339                     // Out of sync.
340                     if (++attempts > 10) {
341                         m_cache->m_log.error("failed to bind client address, update attempts exceeded limit");
342                         throw IOException("Unable to update stored session, exceeded retry limit.");
343                     }
344                     m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
345                     ver = m_cache->m_storage->readText(getID(), "session", &record);
346                     if (!ver) {
347                         m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
348                         throw IOException("Unable to read back stored session.");
349                     }
350 
351                     // Reset object.
352                     DDF newobj;
353                     istringstream in(record);
354                     in >> newobj;
355 
356                     m_ids.clear();
357                     for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
358                     m_attributes.clear();
359                     m_attributeIndex.clear();
360                     newobj["version"].integer(ver);
361                     m_obj.destroy();
362                     m_obj = newobj;
363 
364                     ver = -1;
365                 }
366             } while (ver < 0); // negative indicates a sync issue so we retry
367         }
368 #else
369         throw ConfigurationException("Session touch requires a StorageService.");
370 #endif
371     }
372 
373     m_lastAccess = now;
374 }
375 
376 #ifndef SHIBSP_LITE
377 
addAttributes(const vector<Attribute * > & attributes)378 void StoredSession::addAttributes(const vector<Attribute*>& attributes)
379 {
380 #ifdef _DEBUG
381     xmltooling::NDC ndc("addAttributes");
382 #endif
383 
384     if (!m_cache->m_storage)
385         throw ConfigurationException("Session modification requires a StorageService.");
386 
387     m_cache->m_log.debug("adding attributes to session (%s)", getID());
388 
389     int ver;
390     short attempts = 0;
391     do {
392         DDF attr;
393         DDF attrs = m_obj["attributes"];
394         if (!attrs.islist())
395             attrs = m_obj.addmember("attributes").list();
396         for (vector<Attribute*>::const_iterator a=attributes.begin(); a!=attributes.end(); ++a) {
397             attr = (*a)->marshall();
398             attrs.add(attr);
399         }
400 
401         // Tentatively increment the version.
402         m_obj["version"].integer(m_obj["version"].integer()+1);
403 
404         ostringstream str;
405         str << m_obj;
406         string record(str.str());
407 
408         try {
409             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
410         }
411         catch (std::exception&) {
412             // Roll back modification to record.
413             m_obj["version"].integer(m_obj["version"].integer()-1);
414             vector<Attribute*>::size_type count = attributes.size();
415             while (count--)
416                 attrs.last().destroy();
417             throw;
418         }
419 
420         if (ver <= 0) {
421             // Roll back modification to record.
422             m_obj["version"].integer(m_obj["version"].integer()-1);
423             vector<Attribute*>::size_type count = attributes.size();
424             while (count--)
425                 attrs.last().destroy();
426         }
427         if (!ver) {
428             // Fatal problem with update.
429             throw IOException("Unable to update stored session.");
430         }
431         else if (ver < 0) {
432             // Out of sync.
433             if (++attempts > 10) {
434                 m_cache->m_log.error("failed to update stored session, update attempts exceeded limit");
435                 throw IOException("Unable to update stored session, exceeded retry limit.");
436             }
437             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
438             ver = m_cache->m_storage->readText(getID(), "session", &record);
439             if (!ver) {
440                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
441                 throw IOException("Unable to read back stored session.");
442             }
443 
444             // Reset object.
445             DDF newobj;
446             istringstream in(record);
447             in >> newobj;
448 
449             m_ids.clear();
450             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
451             m_attributes.clear();
452             m_attributeIndex.clear();
453             newobj["version"].integer(ver);
454             m_obj.destroy();
455             m_obj = newobj;
456 
457             ver = -1;
458         }
459     } while (ver < 0);  // negative indicates a sync issue so we retry
460 
461     // We own them now, so clean them up.
462     for_each(attributes.begin(), attributes.end(), xmltooling::cleanup<Attribute>());
463 }
464 
getAssertion(const char * id) const465 const Assertion* StoredSession::getAssertion(const char* id) const
466 {
467     if (!m_cache->m_storage)
468         throw ConfigurationException("Assertion retrieval requires a StorageService.");
469 
470     map< string,boost::shared_ptr<Assertion> >::const_iterator i = m_tokens.find(id);
471     if (i != m_tokens.end())
472         return i->second.get();
473 
474     string tokenstr;
475     if (!m_cache->m_storage->readText(getID(), id, &tokenstr))
476         throw FatalProfileException("Assertion not found in cache.");
477 
478     // Parse and bind the document into an XMLObject.
479     istringstream instr(tokenstr);
480     DOMDocument* doc = XMLToolingConfig::getConfig().getParser().parse(instr);
481     XercesJanitor<DOMDocument> janitor(doc);
482     boost::shared_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
483     janitor.release();
484 
485     boost::shared_ptr<Assertion> token = dynamic_pointer_cast<Assertion,XMLObject>(xmlObject);
486     if (!token)
487         throw FatalProfileException("Request for cached assertion returned an unknown object type.");
488 
489     m_tokens[id] = token;
490     return token.get();
491 }
492 
addAssertion(Assertion * assertion)493 void StoredSession::addAssertion(Assertion* assertion)
494 {
495 #ifdef _DEBUG
496     xmltooling::NDC ndc("addAssertion");
497 #endif
498 
499     if (!m_cache->m_storage)
500         throw ConfigurationException("Session modification requires a StorageService.");
501     else if (!assertion)
502         throw FatalProfileException("Unknown object type passed to session for storage.");
503 
504     auto_ptr_char id(assertion->getID());
505     if (!id.get() || !*id.get())
506         throw IOException("Assertion did not carry an ID.");
507     else if (strlen(id.get()) > m_cache->m_storage->getCapabilities().getKeySize())
508         throw IOException("Assertion ID ($1) exceeds allowable storage key size.", params(1, id.get()));
509 
510     m_cache->m_log.debug("adding assertion (%s) to session (%s)", id.get(), getID());
511 
512     time_t exp = 0;
513     if (!m_cache->m_storage->readText(getID(), "session", nullptr, &exp) || exp == 0)
514         throw IOException("Unable to load expiration time for stored session.");
515 
516     ostringstream tokenstr;
517     tokenstr << *assertion;
518     if (!m_cache->m_storage->createText(getID(), id.get(), tokenstr.str().c_str(), exp))
519         throw IOException("Attempted to insert duplicate assertion ID into session.");
520 
521     int ver;
522     short attempts = 0;
523     do {
524         DDF token = DDF(nullptr).string(id.get());
525         m_obj["assertions"].add(token);
526 
527         // Tentatively increment the version.
528         m_obj["version"].integer(m_obj["version"].integer() + 1);
529 
530         ostringstream str;
531         str << m_obj;
532         string record(str.str());
533 
534         try {
535             ver = m_cache->m_storage->updateText(getID(), "session", record.c_str(), 0, m_obj["version"].integer()-1);
536         }
537         catch (std::exception&) {
538             token.destroy();
539             m_obj["version"].integer(m_obj["version"].integer() - 1);
540             m_cache->m_storage->deleteText(getID(), id.get());
541             throw;
542         }
543 
544         if (ver <= 0) {
545             token.destroy();
546             m_obj["version"].integer(m_obj["version"].integer()-1);
547         }
548         if (!ver) {
549             // Fatal problem with update.
550             m_cache->m_log.error("updateText failed on StorageService for session (%s)", getID());
551             m_cache->m_storage->deleteText(getID(), id.get());
552             throw IOException("Unable to update stored session.");
553         }
554         else if (ver < 0) {
555             // Out of sync.
556             if (++attempts > 10) {
557                 m_cache->m_log.error("failed to update stored session, update attempts exceeded limit");
558                 throw IOException("Unable to update stored session, exceeded retry limit.");
559             }
560             m_cache->m_log.warn("storage service indicates the record is out of sync, updating with a fresh copy...");
561             ver = m_cache->m_storage->readText(getID(), "session", &record);
562             if (!ver) {
563                 m_cache->m_log.error("readText failed on StorageService for session (%s)", getID());
564                 m_cache->m_storage->deleteText(getID(), id.get());
565                 throw IOException("Unable to read back stored session.");
566             }
567 
568             // Reset object.
569             DDF newobj;
570             istringstream in(record);
571             in >> newobj;
572 
573             m_ids.clear();
574             for_each(m_attributes.begin(), m_attributes.end(), xmltooling::cleanup<Attribute>());
575             m_attributes.clear();
576             m_attributeIndex.clear();
577             newobj["version"].integer(ver);
578             m_obj.destroy();
579             m_obj = newobj;
580 
581             ver = -1;
582         }
583     } while (ver < 0); // negative indicates a sync issue so we retry
584 
585     m_ids.clear();
586     delete assertion;
587 }
588 
589 #endif
590 
591