Add provider Id, track last update, report in status method.
[shibboleth/cpp-opensaml.git] / saml / saml2 / metadata / impl / XMLMetadataProvider.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  * XMLMetadataProvider.cpp
23  *
24  * Supplies metadata from an XML file
25  */
26
27 #include "internal.h"
28 #include "binding/SAMLArtifact.h"
29 #include "saml2/metadata/Metadata.h"
30 #include "saml2/metadata/MetadataFilter.h"
31 #include "saml2/metadata/AbstractMetadataProvider.h"
32 #include "saml2/metadata/DiscoverableMetadataProvider.h"
33
34 #include <fstream>
35 #include <xmltooling/XMLToolingConfig.h>
36 #include <xmltooling/io/HTTPResponse.h>
37 #include <xmltooling/util/NDC.h>
38 #include <xmltooling/util/PathResolver.h>
39 #include <xmltooling/util/ReloadableXMLFile.h>
40 #include <xmltooling/util/Threads.h>
41 #include <xmltooling/validation/ValidatorSuite.h>
42
43 #if defined(OPENSAML_LOG4SHIB)
44 # include <log4shib/NDC.hh>
45 #elif defined(OPENSAML_LOG4CPP)
46 # include <log4cpp/NDC.hh>
47 #endif
48
49 using namespace opensaml::saml2md;
50 using namespace xmltooling::logging;
51 using namespace xmltooling;
52 using namespace std;
53
54 #if defined (_MSC_VER)
55     #pragma warning( push )
56     #pragma warning( disable : 4250 )
57 #endif
58
59 namespace opensaml {
60     namespace saml2md {
61
62         class SAML_DLLLOCAL XMLMetadataProvider
63             : public AbstractMetadataProvider, public DiscoverableMetadataProvider, public ReloadableXMLFile
64         {
65         public:
66             XMLMetadataProvider(const DOMElement* e);
67
68             virtual ~XMLMetadataProvider() {
69                 shutdown();
70                 delete m_object;
71             }
72
73             void init() {
74                 try {
75                     if (!m_id.empty()) {
76                         string threadid("[");
77                         threadid += m_id + ']';
78                         logging::NDC::push(threadid);
79                     }
80                     background_load();
81                     startup();
82                 }
83                 catch (...) {
84                     startup();
85                     if (!m_id.empty()) {
86                         logging::NDC::pop();
87                     }
88                     throw;
89                 }
90
91                 if (!m_id.empty()) {
92                     logging::NDC::pop();
93                 }
94             }
95
96             const char* getId() const {
97                 return m_id.c_str();
98             }
99
100             const XMLObject* getMetadata() const {
101                 return m_object;
102             }
103
104         protected:
105             pair<bool,DOMElement*> load(bool backup);
106             pair<bool,DOMElement*> background_load();
107
108         private:
109             using AbstractMetadataProvider::index;
110             void index(time_t& validUntil);
111             time_t computeNextRefresh();
112
113             XMLObject* m_object;
114             bool m_discoveryFeed;
115             double m_refreshDelayFactor;
116             unsigned int m_backoffFactor;
117             time_t m_minRefreshDelay,m_maxRefreshDelay,m_lastValidUntil;
118         };
119
120         MetadataProvider* SAML_DLLLOCAL XMLMetadataProviderFactory(const DOMElement* const & e)
121         {
122             return new XMLMetadataProvider(e);
123         }
124
125         static const XMLCh discoveryFeed[] =        UNICODE_LITERAL_13(d,i,s,c,o,v,e,r,y,F,e,e,d);
126         static const XMLCh minRefreshDelay[] =      UNICODE_LITERAL_15(m,i,n,R,e,f,r,e,s,h,D,e,l,a,y);
127         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);
128     };
129 };
130
131 #if defined (_MSC_VER)
132     #pragma warning( pop )
133 #endif
134
135 XMLMetadataProvider::XMLMetadataProvider(const DOMElement* e)
136     : MetadataProvider(e), AbstractMetadataProvider(e), DiscoverableMetadataProvider(e),
137         ReloadableXMLFile(e, Category::getInstance(SAML_LOGCAT".MetadataProvider.XML"), false),
138         m_object(nullptr), m_discoveryFeed(XMLHelper::getAttrBool(e, true, discoveryFeed)),
139         m_refreshDelayFactor(0.75), m_backoffFactor(1),
140         m_minRefreshDelay(XMLHelper::getAttrInt(e, 600, minRefreshDelay)),
141         m_maxRefreshDelay(m_reloadInterval), m_lastValidUntil(SAMLTIME_MAX)
142 {
143     if (!m_local && m_maxRefreshDelay) {
144         const XMLCh* setting = e->getAttributeNS(nullptr, refreshDelayFactor);
145         if (setting && *setting) {
146             auto_ptr_char delay(setting);
147             m_refreshDelayFactor = atof(delay.get());
148             if (m_refreshDelayFactor <= 0.0 || m_refreshDelayFactor >= 1.0) {
149                 m_log.error("invalid refreshDelayFactor setting, using default");
150                 m_refreshDelayFactor = 0.75;
151             }
152         }
153
154         if (m_minRefreshDelay > m_maxRefreshDelay) {
155             m_log.warn("minRefreshDelay setting exceeds maxRefreshDelay/reloadInterval setting, lowering to match it");
156             m_minRefreshDelay = m_maxRefreshDelay;
157         }
158     }
159 }
160
161 pair<bool,DOMElement*> XMLMetadataProvider::load(bool backup)
162 {
163     if (!backup) {
164         // Lower the refresh rate in case of an error.
165         m_reloadInterval = m_minRefreshDelay;
166     }
167
168     // Call the base class to load/parse the appropriate XML resource.
169     pair<bool,DOMElement*> raw = ReloadableXMLFile::load(backup);
170
171     // If we own it, wrap it for now.
172     XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : nullptr);
173
174     // Unmarshall objects, binding the document.
175     auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(raw.second, true));
176     docjanitor.release();
177
178     if (!dynamic_cast<const EntitiesDescriptor*>(xmlObject.get()) && !dynamic_cast<const EntityDescriptor*>(xmlObject.get()))
179         throw MetadataException(
180             "Root of metadata instance not recognized: $1", params(1,xmlObject->getElementQName().toString().c_str())
181             );
182
183     // Preprocess the metadata (even if we schema-validated).
184     try {
185         SchemaValidators.validate(xmlObject.get());
186     }
187     catch (exception& ex) {
188         m_log.error("metadata intance failed manual validation checking: %s", ex.what());
189         throw MetadataException("Metadata instance failed manual validation checking.");
190     }
191
192     // This is the best place to take a backup, since it's superficially "correct" metadata.
193     string backupKey;
194     if (!backup && !m_backing.empty()) {
195         // We compute a random filename extension to the "real" location.
196         SAMLConfig::getConfig().generateRandomBytes(backupKey, 2);
197         backupKey = m_backing + '.' + SAMLArtifact::toHex(backupKey);
198         m_log.debug("backing up remote metadata resource to (%s)", backupKey.c_str());
199         try {
200             ofstream backer(backupKey.c_str());
201             backer << *(raw.second->getOwnerDocument());
202         }
203         catch (exception& ex) {
204             m_log.crit("exception while backing up metadata: %s", ex.what());
205             backupKey.erase();
206         }
207     }
208
209     try {
210         doFilters(*xmlObject.get());
211     }
212     catch (exception&) {
213         if (!backupKey.empty())
214             remove(backupKey.c_str());
215         throw;
216     }
217
218     if (!backupKey.empty()) {
219         m_log.debug("committing backup file to permanent location (%s)", m_backing.c_str());
220         Locker locker(getBackupLock());
221         remove(m_backing.c_str());
222         if (rename(backupKey.c_str(), m_backing.c_str()) != 0)
223             m_log.crit("unable to rename metadata backup file");
224         preserveCacheTag();
225     }
226
227     xmlObject->releaseThisAndChildrenDOM();
228     xmlObject->setDocument(nullptr);
229
230     // Swap it in after acquiring write lock if necessary.
231     if (m_lock)
232         m_lock->wrlock();
233     SharedLock locker(m_lock, false);
234     bool changed = m_object!=nullptr;
235     delete m_object;
236     m_object = xmlObject.release();
237     m_lastValidUntil = SAMLTIME_MAX;
238     index(m_lastValidUntil);
239     if (m_discoveryFeed)
240         generateFeed();
241     if (changed)
242         emitChangeEvent();
243     m_lastUpdate = time(nullptr);
244
245     // Tracking cacheUntil through the tree is TBD, but
246     // validUntil is the tightest interval amongst the children.
247
248     // If a remote resource that's monitored, adjust the reload interval.
249     if (!backup && !m_local && m_lock) {
250         m_backoffFactor = 1;
251         m_reloadInterval = computeNextRefresh();
252         m_log.info("adjusted reload interval to %d seconds", m_reloadInterval);
253     }
254
255     m_loaded = true;
256     return make_pair(false,(DOMElement*)nullptr);
257 }
258
259 pair<bool,DOMElement*> XMLMetadataProvider::background_load()
260 {
261     try {
262         return load(false);
263     }
264     catch (long& ex) {
265         if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
266             // Unchanged document, so re-establish previous refresh interval.
267             m_reloadInterval = computeNextRefresh();
268             m_log.info("remote resource (%s) unchanged, adjusted reload interval to %u seconds", m_source.c_str(), m_reloadInterval);
269         }
270         else {
271             // Any other status code, just treat as an error.
272             m_reloadInterval = m_minRefreshDelay * m_backoffFactor++;
273             if (m_reloadInterval > m_maxRefreshDelay)
274                 m_reloadInterval = m_maxRefreshDelay;
275             m_log.warn("adjusted reload interval to %u seconds", m_reloadInterval);
276         }
277         if (!m_loaded && !m_backing.empty())
278             return load(true);
279         throw;
280     }
281     catch (exception&) {
282         if (!m_local) {
283             m_reloadInterval = m_minRefreshDelay * m_backoffFactor++;
284             if (m_reloadInterval > m_maxRefreshDelay)
285                 m_reloadInterval = m_maxRefreshDelay;
286             m_log.warn("adjusted reload interval to %u seconds", m_reloadInterval);
287             if (!m_loaded && !m_backing.empty())
288                 return load(true);
289         }
290         throw;
291     }
292 }
293
294 time_t XMLMetadataProvider::computeNextRefresh()
295 {
296     time_t now = time(nullptr);
297
298     // If some or all of the metadata is already expired, reload after the minimum.
299     if (m_lastValidUntil < now) {
300         return m_minRefreshDelay;
301     }
302     else {
303         // Compute the smaller of the validUntil / cacheDuration constraints.
304         time_t ret = m_lastValidUntil - now;
305         const CacheableSAMLObject* cacheable = dynamic_cast<const CacheableSAMLObject*>(m_object);
306         if (cacheable && cacheable->getCacheDuration())
307             ret = min(ret, cacheable->getCacheDurationEpoch());
308             
309         // Adjust for the delay factor.
310         ret *= m_refreshDelayFactor;
311
312         // Bound by max and min.
313         if (ret > m_maxRefreshDelay)
314             return m_maxRefreshDelay;
315         else if (ret < m_minRefreshDelay)
316             return m_minRefreshDelay;
317
318         return ret;
319     }
320 }
321
322 void XMLMetadataProvider::index(time_t& validUntil)
323 {
324     clearDescriptorIndex();
325     EntitiesDescriptor* group=dynamic_cast<EntitiesDescriptor*>(m_object);
326     if (group) {
327         indexGroup(group, validUntil);
328         return;
329     }
330     indexEntity(dynamic_cast<EntityDescriptor*>(m_object), validUntil);
331 }