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