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