SSPCPP-616 - clean up concatenated string literals
[shibboleth/cpp-opensaml.git] / saml / saml2 / metadata / impl / DynamicMetadataProvider.cpp
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  * DynamicMetadataProvider.cpp
23  *
24  * Simple implementation of a dynamic caching MetadataProvider.
25  */
26
27 #include "internal.h"
28 #include "binding/SAMLArtifact.h"
29 #include "saml2/metadata/Metadata.h"
30 #include "saml2/metadata/DynamicMetadataProvider.h"
31
32 #include <xercesc/framework/Wrapper4InputSource.hpp>
33 #include <xercesc/util/XMLUniDefs.hpp>
34 #include <xmltooling/logging.h>
35 #include <xmltooling/XMLToolingConfig.h>
36 #include <xmltooling/util/ParserPool.h>
37 #include <xmltooling/util/Threads.h>
38 #include <xmltooling/util/XMLHelper.h>
39 #include <xmltooling/validation/ValidatorSuite.h>
40
41 using namespace opensaml::saml2md;
42 using namespace xmltooling::logging;
43 using namespace xmltooling;
44 using namespace std;
45
46 # ifndef min
47 #  define min(a,b)            (((a) < (b)) ? (a) : (b))
48 # endif
49
50 static const XMLCh id[] =                   UNICODE_LITERAL_2(i,d);
51 static const XMLCh maxCacheDuration[] =     UNICODE_LITERAL_16(m,a,x,C,a,c,h,e,D,u,r,a,t,i,o,n);
52 static const XMLCh minCacheDuration[] =     UNICODE_LITERAL_16(m,i,n,C,a,c,h,e,D,u,r,a,t,i,o,n);
53 static const XMLCh refreshDelayFactor[] =   UNICODE_LITERAL_18(r,e,f,r,e,s,h,D,e,l,a,y,F,a,c,t,o,r);
54 static const XMLCh validate[] =             UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
55
56 namespace opensaml {
57     namespace saml2md {
58         MetadataProvider* SAML_DLLLOCAL DynamicMetadataProviderFactory(const DOMElement* const & e)
59         {
60             return new DynamicMetadataProvider(e);
61         }
62     };
63 };
64
65 DynamicMetadataProvider::DynamicMetadataProvider(const DOMElement* e)
66     : AbstractMetadataProvider(e),
67       m_validate(XMLHelper::getAttrBool(e, false, validate)),
68         m_id(XMLHelper::getAttrString(e, "Dynamic", id)),
69         m_lock(RWLock::create()),
70         m_refreshDelayFactor(0.75),
71         m_minCacheDuration(XMLHelper::getAttrInt(e, 600, minCacheDuration)),
72         m_maxCacheDuration(XMLHelper::getAttrInt(e, 28800, maxCacheDuration))
73 {
74     if (m_minCacheDuration > m_maxCacheDuration) {
75         Category::getInstance(SAML_LOGCAT ".MetadataProvider.Dynamic").error(
76             "minCacheDuration setting exceeds maxCacheDuration setting, lowering to match it"
77             );
78         m_minCacheDuration = m_maxCacheDuration;
79     }
80
81     const XMLCh* delay = e ? e->getAttributeNS(nullptr, refreshDelayFactor) : nullptr;
82     if (delay && *delay) {
83         auto_ptr_char temp(delay);
84         m_refreshDelayFactor = atof(temp.get());
85         if (m_refreshDelayFactor <= 0.0 || m_refreshDelayFactor >= 1.0) {
86             Category::getInstance(SAML_LOGCAT ".MetadataProvider.Dynamic").error(
87                 "invalid refreshDelayFactor setting, using default"
88                 );
89             m_refreshDelayFactor = 0.75;
90         }
91     }
92 }
93
94 DynamicMetadataProvider::~DynamicMetadataProvider()
95 {
96     // Each entity in the map is unique (no multimap semantics), so this is safe.
97     clearDescriptorIndex(true);
98 }
99
100 const XMLObject* DynamicMetadataProvider::getMetadata() const
101 {
102     throw MetadataException("getMetadata operation not implemented on this provider.");
103 }
104
105 Lockable* DynamicMetadataProvider::lock()
106 {
107     m_lock->rdlock();
108     return this;
109 }
110
111 void DynamicMetadataProvider::unlock()
112 {
113     m_lock->unlock();
114 }
115
116 void DynamicMetadataProvider::init()
117 {
118 }
119
120 const char* DynamicMetadataProvider::getId() const
121 {
122     return m_id.c_str();
123 }
124
125 pair<const EntityDescriptor*,const RoleDescriptor*> DynamicMetadataProvider::getEntityDescriptor(const Criteria& criteria) const
126 {
127     Category& log = Category::getInstance(SAML_LOGCAT ".MetadataProvider.Dynamic");
128
129     // First we check the underlying cache.
130     pair<const EntityDescriptor*,const RoleDescriptor*> entity = AbstractMetadataProvider::getEntityDescriptor(criteria);
131
132     // Check to see if we're within the caching interval for a lookup of this entity.
133     // This applies *even if we didn't get a hit* because the cache map tracks failed
134     // lookups also, to prevent constant reload attempts.
135     cachemap_t::iterator cit;
136     if (entity.first) {
137         cit = m_cacheMap.find(entity.first->getEntityID());
138     }
139     else if (criteria.entityID_ascii) {
140         auto_ptr_XMLCh widetemp(criteria.entityID_ascii);
141         cit = m_cacheMap.find(widetemp.get());
142     }
143     else if (criteria.entityID_unicode) {
144         cit = m_cacheMap.find(criteria.entityID_unicode);
145     }
146     else if (criteria.artifact) {
147         auto_ptr_XMLCh widetemp(criteria.artifact->getSource().c_str());
148         cit = m_cacheMap.find(widetemp.get());
149     }
150     else {
151         cit = m_cacheMap.end();
152     }
153     if (cit != m_cacheMap.end()) {
154         if (time(nullptr) <= cit->second)
155             return entity;
156         m_cacheMap.erase(cit);
157     }
158
159     string name;
160     if (criteria.entityID_ascii) {
161         name = criteria.entityID_ascii;
162     }
163     else if (criteria.entityID_unicode) {
164         auto_ptr_char temp(criteria.entityID_unicode);
165         name = temp.get();
166     }
167     else if (criteria.artifact) {
168         name = criteria.artifact->getSource();
169     }
170     else {
171         return entity;
172     }
173
174     if (entity.first)
175         log.info("metadata for (%s) is beyond caching interval, attempting to refresh", name.c_str());
176     else
177         log.info("resolving metadata for (%s)", name.c_str());
178
179     try {
180         // Try resolving it.
181         auto_ptr<EntityDescriptor> entity2(resolve(criteria));
182
183         // Verify the entityID.
184         if (criteria.entityID_unicode && !XMLString::equals(criteria.entityID_unicode, entity2->getEntityID())) {
185             log.error("metadata instance did not match expected entityID");
186             return entity;
187         }
188         else {
189             auto_ptr_XMLCh temp2(name.c_str());
190             if (!XMLString::equals(temp2.get(), entity2->getEntityID())) {
191                 log.error("metadata instance did not match expected entityID");
192                 return entity;
193             }
194         }
195
196         // Preprocess the metadata (even if we schema-validated).
197         try {
198             SchemaValidators.validate(entity2.get());
199         }
200         catch (exception& ex) {
201             log.error("metadata intance failed manual validation checking: %s", ex.what());
202             throw MetadataException("Metadata instance failed manual validation checking.");
203         }
204
205         // Filter it, which may throw.
206         doFilters(*entity2);
207
208         time_t now = time(nullptr);
209         if (entity2->getValidUntil() && entity2->getValidUntilEpoch() < now + 60)
210             throw MetadataException("Metadata was already invalid at the time of retrieval.");
211
212         log.info("caching resolved metadata for (%s)", name.c_str());
213
214         // Compute the smaller of the validUntil / cacheDuration constraints.
215         time_t cacheExp = (entity2->getValidUntil() ? entity2->getValidUntilEpoch() : SAMLTIME_MAX) - now;
216         if (entity2->getCacheDuration())
217             cacheExp = min(cacheExp, entity2->getCacheDurationEpoch());
218             
219         // Adjust for the delay factor.
220         cacheExp *= m_refreshDelayFactor;
221
222         // Bound by max and min.
223         if (cacheExp > m_maxCacheDuration)
224             cacheExp = m_maxCacheDuration;
225         else if (cacheExp < m_minCacheDuration)
226             cacheExp = m_minCacheDuration;
227
228         log.info("next refresh of metadata for (%s) no sooner than %u seconds", name.c_str(), cacheExp);
229
230         // Upgrade our lock so we can cache the new metadata.
231         m_lock->unlock();
232         m_lock->wrlock();
233
234         // Notify observers.
235         emitChangeEvent(*entity2);
236
237         // Record the proper refresh time.
238         m_cacheMap[entity2->getEntityID()] = now + cacheExp;
239
240         // Make sure we clear out any existing copies, including stale metadata or if somebody snuck in.
241         cacheExp = SAMLTIME_MAX;
242         indexEntity(entity2.get(), cacheExp, true);
243         entity2.release();
244
245         m_lastUpdate = now;
246
247         // Downgrade back to a read lock.
248         m_lock->unlock();
249         m_lock->rdlock();
250     }
251     catch (exception& e) {
252         log.error("error while resolving entityID (%s): %s", name.c_str(), e.what());
253         // This will return entries that are beyond their cache period,
254         // but not beyond their validity unless that criteria option was set.
255         // If it is a cache-expired entry, bump the cache period to prevent retries.
256         if (entity.first)
257             m_cacheMap[entity.first->getEntityID()] = time(nullptr) + m_minCacheDuration;
258         else if (criteria.entityID_unicode)
259             m_cacheMap[criteria.entityID_unicode] = time(nullptr) + m_minCacheDuration;
260         else {
261             auto_ptr_XMLCh widetemp(name.c_str());
262             m_cacheMap[widetemp.get()] = time(nullptr) + m_minCacheDuration;
263         }
264         log.warn("next refresh of metadata for (%s) no sooner than %u seconds", name.c_str(), m_minCacheDuration);
265         return entity;
266     }
267
268     // Rinse and repeat.
269     return getEntityDescriptor(criteria);
270 }
271
272 EntityDescriptor* DynamicMetadataProvider::resolve(const Criteria& criteria) const
273 {
274     string name;
275     if (criteria.entityID_ascii) {
276         name = criteria.entityID_ascii;
277     }
278     else if (criteria.entityID_unicode) {
279         auto_ptr_char temp(criteria.entityID_unicode);
280         name = temp.get();
281     }
282     else if (criteria.artifact) {
283         throw MetadataException("Unable to resolve metadata dynamically from an artifact.");
284     }
285
286     try {
287         DOMDocument* doc=nullptr;
288         auto_ptr_XMLCh widenit(name.c_str());
289         URLInputSource src(widenit.get());
290         Wrapper4InputSource dsrc(&src,false);
291         if (m_validate)
292             doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
293         else
294             doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
295
296         // Wrap the document for now.
297         XercesJanitor<DOMDocument> docjanitor(doc);
298
299         // Unmarshall objects, binding the document.
300         auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
301         docjanitor.release();
302
303         // Make sure it's metadata.
304         EntityDescriptor* entity = dynamic_cast<EntityDescriptor*>(xmlObject.get());
305         if (!entity) {
306             throw MetadataException(
307                 "Root of metadata instance not recognized: $1", params(1,xmlObject->getElementQName().toString().c_str())
308                 );
309         }
310         xmlObject.release();
311         return entity;
312     }
313     catch (XMLException& e) {
314         auto_ptr_char msg(e.getMessage());
315         Category::getInstance(SAML_LOGCAT ".MetadataProvider.Dynamic").error(
316             "Xerces error while resolving entityID (%s): %s", name.c_str(), msg.get()
317             );
318         throw MetadataException(msg.get());
319     }
320 }