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