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