9a65d012af68b11bc9306d181dffb8e7057a007b
[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     delete m_lock;
99 }
100
101 const XMLObject* DynamicMetadataProvider::getMetadata() const
102 {
103     throw MetadataException("getMetadata operation not implemented on this provider.");
104 }
105
106 Lockable* DynamicMetadataProvider::lock()
107 {
108     m_lock->rdlock();
109     return this;
110 }
111
112 void DynamicMetadataProvider::unlock()
113 {
114     m_lock->unlock();
115 }
116
117 void DynamicMetadataProvider::init()
118 {
119 }
120
121 const char* DynamicMetadataProvider::getId() const
122 {
123     return m_id.c_str();
124 }
125
126 pair<const EntityDescriptor*,const RoleDescriptor*> DynamicMetadataProvider::getEntityDescriptor(const Criteria& criteria) const
127 {
128     Category& log = Category::getInstance(SAML_LOGCAT".MetadataProvider.Dynamic");
129
130     // First we check the underlying cache.
131     pair<const EntityDescriptor*,const RoleDescriptor*> entity = AbstractMetadataProvider::getEntityDescriptor(criteria);
132
133     // Check to see if we're within the caching interval for a lookup of this entity.
134     // This applies *even if we didn't get a hit* because the cache map tracks failed
135     // lookups also, to prevent constant reload attempts.
136     cachemap_t::iterator cit;
137     if (entity.first) {
138         cit = m_cacheMap.find(entity.first->getEntityID());
139     }
140     else if (criteria.entityID_ascii) {
141         auto_ptr_XMLCh widetemp(criteria.entityID_ascii);
142         cit = m_cacheMap.find(widetemp.get());
143     }
144     else if (criteria.entityID_unicode) {
145         cit = m_cacheMap.find(criteria.entityID_unicode);
146     }
147     else if (criteria.artifact) {
148         auto_ptr_XMLCh widetemp(criteria.artifact->getSource().c_str());
149         cit = m_cacheMap.find(widetemp.get());
150     }
151     else {
152         cit = m_cacheMap.end();
153     }
154     if (cit != m_cacheMap.end()) {
155         if (time(nullptr) <= cit->second)
156             return entity;
157         m_cacheMap.erase(cit);
158     }
159
160     string name;
161     if (criteria.entityID_ascii) {
162         name = criteria.entityID_ascii;
163     }
164     else if (criteria.entityID_unicode) {
165         auto_ptr_char temp(criteria.entityID_unicode);
166         name = temp.get();
167     }
168     else if (criteria.artifact) {
169         name = criteria.artifact->getSource();
170     }
171     else {
172         return entity;
173     }
174
175     if (entity.first)
176         log.info("metadata for (%s) is beyond caching interval, attempting to refresh", name.c_str());
177     else
178         log.info("resolving metadata for (%s)", name.c_str());
179
180     try {
181         // Try resolving it.
182         auto_ptr<EntityDescriptor> entity2(resolve(criteria));
183
184         // Verify the entityID.
185         if (criteria.entityID_unicode && !XMLString::equals(criteria.entityID_unicode, entity2->getEntityID())) {
186             log.error("metadata instance did not match expected entityID");
187             return entity;
188         }
189         else {
190             auto_ptr_XMLCh temp2(name.c_str());
191             if (!XMLString::equals(temp2.get(), entity2->getEntityID())) {
192                 log.error("metadata instance did not match expected entityID");
193                 return entity;
194             }
195         }
196
197         // Preprocess the metadata (even if we schema-validated).
198         try {
199             SchemaValidators.validate(entity2.get());
200         }
201         catch (exception& ex) {
202             log.error("metadata intance failed manual validation checking: %s", ex.what());
203             throw MetadataException("Metadata instance failed manual validation checking.");
204         }
205
206         // Filter it, which may throw.
207         doFilters(*entity2.get());
208
209         time_t now = time(nullptr);
210         if (entity2->getValidUntil() && entity2->getValidUntilEpoch() < now + 60)
211             throw MetadataException("Metadata was already invalid at the time of retrieval.");
212
213         log.info("caching resolved metadata for (%s)", name.c_str());
214
215         // Compute the smaller of the validUntil / cacheDuration constraints.
216         time_t cacheExp = (entity2->getValidUntil() ? entity2->getValidUntilEpoch() : SAMLTIME_MAX) - now;
217         if (entity2->getCacheDuration())
218             cacheExp = min(cacheExp, entity2->getCacheDurationEpoch());
219             
220         // Adjust for the delay factor.
221         cacheExp *= m_refreshDelayFactor;
222
223         // Bound by max and min.
224         if (cacheExp > m_maxCacheDuration)
225             cacheExp = m_maxCacheDuration;
226         else if (cacheExp < m_minCacheDuration)
227             cacheExp = m_minCacheDuration;
228
229         log.info("next refresh of metadata for (%s) no sooner than %u seconds", name.c_str(), cacheExp);
230
231         // Upgrade our lock so we can cache the new metadata.
232         m_lock->unlock();
233         m_lock->wrlock();
234
235         // Notify observers.
236         emitChangeEvent();
237
238         // Record the proper refresh time.
239         m_cacheMap[entity2->getEntityID()] = now + cacheExp;
240
241         // Make sure we clear out any existing copies, including stale metadata or if somebody snuck in.
242         cacheExp = SAMLTIME_MAX;
243         indexEntity(entity2.release(), cacheExp, true);
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 }