b6be0fe89f0d0654de776ea028aec34c5544f0c1
[shibboleth/cpp-xmltooling.git] / xmltooling / util / ReloadableXMLFile.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  * @file ReloadableXMLFile.cpp
19  *
20  * Base class for file-based XML configuration.
21  */
22
23 #include "internal.h"
24 #include "io/HTTPResponse.h"
25 #ifndef XMLTOOLING_LITE
26 # include "security/Credential.h"
27 # include "security/CredentialCriteria.h"
28 # include "security/CredentialResolver.h"
29 # include "security/SignatureTrustEngine.h"
30 # include "signature/Signature.h"
31 # include "signature/SignatureValidator.h"
32 #endif
33 #include "util/NDC.h"
34 #include "util/PathResolver.h"
35 #include "util/ReloadableXMLFile.h"
36 #include "util/Threads.h"
37 #include "util/XMLConstants.h"
38 #include "util/XMLHelper.h"
39
40 #if defined(XMLTOOLING_LOG4SHIB)
41 # include <log4shib/NDC.hh>
42 #elif defined(XMLTOOLING_LOG4CPP)
43 # include <log4cpp/NDC.hh>
44 #endif
45
46 #include <memory>
47 #include <fstream>
48 #include <sys/types.h>
49 #include <sys/stat.h>
50
51 #include <xercesc/framework/LocalFileInputSource.hpp>
52 #include <xercesc/framework/Wrapper4InputSource.hpp>
53 #include <xercesc/util/XMLUniDefs.hpp>
54
55 #ifndef XMLTOOLING_LITE
56 # include <xsec/dsig/DSIGReference.hpp>
57 # include <xsec/dsig/DSIGTransformList.hpp>
58 using namespace xmlsignature;
59 #endif
60
61 using namespace xmltooling::logging;
62 using namespace xmltooling;
63 using namespace xercesc;
64 using namespace std;
65
66 #ifndef XMLTOOLING_LITE
67 namespace {
68     class XMLTOOL_DLLLOCAL DummyCredentialResolver : public CredentialResolver
69     {
70     public:
71         DummyCredentialResolver() {}
72         ~DummyCredentialResolver() {}
73
74         Lockable* lock() {return this;}
75         void unlock() {}
76
77         const Credential* resolve(const CredentialCriteria* criteria=nullptr) const {return nullptr;}
78         vector<const Credential*>::size_type resolve(
79             vector<const Credential*>& results, const CredentialCriteria* criteria=nullptr
80             ) const {return 0;}
81     };
82 };
83 #endif
84
85 static const XMLCh id[] =               UNICODE_LITERAL_2(i,d);
86 static const XMLCh uri[] =              UNICODE_LITERAL_3(u,r,i);
87 static const XMLCh url[] =              UNICODE_LITERAL_3(u,r,l);
88 static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
89 static const XMLCh pathname[] =         UNICODE_LITERAL_8(p,a,t,h,n,a,m,e);
90 static const XMLCh file[] =             UNICODE_LITERAL_4(f,i,l,e);
91 static const XMLCh filename[] =         UNICODE_LITERAL_8(f,i,l,e,n,a,m,e);
92 static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
93 static const XMLCh reloadChanges[] =    UNICODE_LITERAL_13(r,e,l,o,a,d,C,h,a,n,g,e,s);
94 static const XMLCh reloadInterval[] =   UNICODE_LITERAL_14(r,e,l,o,a,d,I,n,t,e,r,v,a,l);
95 static const XMLCh maxRefreshDelay[] =  UNICODE_LITERAL_15(m,a,x,R,e,f,r,e,s,h,D,e,l,a,y);
96 static const XMLCh backingFilePath[] =  UNICODE_LITERAL_15(b,a,c,k,i,n,g,F,i,l,e,P,a,t,h);
97 static const XMLCh type[] =             UNICODE_LITERAL_4(t,y,p,e);
98 static const XMLCh certificate[] =      UNICODE_LITERAL_11(c,e,r,t,i,f,i,c,a,t,e);
99 static const XMLCh signerName[] =       UNICODE_LITERAL_10(s,i,g,n,e,r,N,a,m,e);
100 static const XMLCh _TrustEngine[] =     UNICODE_LITERAL_11(T,r,u,s,t,E,n,g,i,n,e);
101 static const XMLCh _CredentialResolver[] = UNICODE_LITERAL_18(C,r,e,d,e,n,t,i,a,l,R,e,s,o,l,v,e,r);
102
103
104 ReloadableXMLFile::ReloadableXMLFile(const DOMElement* e, Category& log, bool startReloadThread)
105     : m_root(e), m_local(true), m_validate(false), m_filestamp(0), m_reloadInterval(0),
106       m_lock(nullptr), m_log(log), m_loaded(false),
107 #ifndef XMLTOOLING_LITE
108       m_credResolver(nullptr), m_trust(nullptr),
109 #endif
110       m_shutdown(false), m_reload_wait(nullptr), m_reload_thread(nullptr)
111 {
112 #ifdef _DEBUG
113     NDC ndc("ReloadableXMLFile");
114 #endif
115
116     // Establish source of data...
117     const XMLCh* source=e->getAttributeNS(nullptr,uri);
118     if (!source || !*source) {
119         source=e->getAttributeNS(nullptr,url);
120         if (!source || !*source) {
121             source=e->getAttributeNS(nullptr,path);
122             if (!source || !*source) {
123                 source=e->getAttributeNS(nullptr,pathname);
124                 if (!source || !*source) {
125                     source=e->getAttributeNS(nullptr,file);
126                     if (!source || !*source) {
127                         source=e->getAttributeNS(nullptr,filename);
128                     }
129                 }
130             }
131         }
132         else {
133             m_local=false;
134         }
135     }
136     else {
137         m_local=false;
138     }
139
140     if (source && *source) {
141         m_validate = XMLHelper::getAttrBool(e, false, validate);
142
143         auto_ptr_char temp(source);
144         m_source = temp.get();
145
146         if (!m_local && !strstr(m_source.c_str(),"://")) {
147             log.warn("deprecated usage of uri/url attribute for a local resource, use path instead");
148             m_local = true;
149         }
150
151 #ifndef XMLTOOLING_LITE
152         // Check for signature bits.
153         if (e->hasAttributeNS(nullptr, certificate)) {
154             // Use a file-based credential resolver rooted here.
155             m_credResolver = XMLToolingConfig::getConfig().CredentialResolverManager.newPlugin(FILESYSTEM_CREDENTIAL_RESOLVER, e);
156         }
157         else {
158             const DOMElement* sub = XMLHelper::getFirstChildElement(e, _CredentialResolver);
159             string t(XMLHelper::getAttrString(sub, nullptr, type));
160             if (!t.empty()) {
161                 m_credResolver = XMLToolingConfig::getConfig().CredentialResolverManager.newPlugin(t.c_str(), sub);
162             }
163             else {
164                 sub = XMLHelper::getFirstChildElement(e, _TrustEngine);
165                 t = XMLHelper::getAttrString(sub, nullptr, type);
166                 if (!t.empty()) {
167                     TrustEngine* trust = XMLToolingConfig::getConfig().TrustEngineManager.newPlugin(t.c_str(), sub);
168                     if (!(m_trust = dynamic_cast<SignatureTrustEngine*>(trust))) {
169                         delete trust;
170                         throw XMLToolingException("TrustEngine-based ReloadableXMLFile requires a SignatureTrustEngine plugin.");
171                     }
172
173                     m_signerName = XMLHelper::getAttrString(e, nullptr, signerName);
174                 }
175             }
176         }
177 #endif
178
179         if (m_local) {
180             XMLToolingConfig::getConfig().getPathResolver()->resolve(m_source, PathResolver::XMLTOOLING_CFG_FILE);
181
182             bool flag = XMLHelper::getAttrBool(e, true, reloadChanges);
183             if (flag) {
184 #ifdef WIN32
185                 struct _stat stat_buf;
186                 if (_stat(m_source.c_str(), &stat_buf) == 0)
187 #else
188                 struct stat stat_buf;
189                 if (stat(m_source.c_str(), &stat_buf) == 0)
190 #endif
191                     m_filestamp = stat_buf.st_mtime;
192                 else
193                     throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
194                 m_lock = RWLock::create();
195             }
196             FILE* cfile = fopen(m_source.c_str(), "r");
197             if (cfile)
198                 fclose(cfile);
199             else
200                 throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
201             log.debug("using local resource (%s), will %smonitor for changes", m_source.c_str(), m_lock ? "" : "not ");
202         }
203         else {
204             log.debug("using remote resource (%s)", m_source.c_str());
205             m_backing = XMLHelper::getAttrString(e, nullptr, backingFilePath);
206             if (!m_backing.empty()) {
207                 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_backing, PathResolver::XMLTOOLING_RUN_FILE);
208                 log.debug("backup remote resource to (%s)", m_backing.c_str());
209                 try {
210                     string tagname = m_backing + ".tag";
211                     ifstream backer(tagname.c_str());
212                     if (backer) {
213                         char cachebuf[256];
214                         if (backer.getline(cachebuf, 255)) {
215                             m_cacheTag = cachebuf;
216                             log.debug("loaded initial cache tag (%s)", m_cacheTag.c_str());
217                         }
218                     }
219                 }
220                 catch (exception&) {
221                 }
222             }
223             m_reloadInterval = XMLHelper::getAttrInt(e, 0, reloadInterval);
224             if (m_reloadInterval == 0)
225                 m_reloadInterval = XMLHelper::getAttrInt(e, 0, maxRefreshDelay);
226             if (m_reloadInterval > 0) {
227                 m_log.debug("will reload remote resource at most every %d seconds", m_reloadInterval);
228                 m_lock = RWLock::create();
229             }
230             m_filestamp = time(nullptr);   // assume it gets loaded initially
231         }
232
233         if (startReloadThread)
234             startup();
235     }
236     else {
237         log.debug("no resource uri/path/name supplied, will load inline configuration");
238     }
239
240     m_id = XMLHelper::getAttrString(e, nullptr, id);
241 }
242
243 ReloadableXMLFile::~ReloadableXMLFile()
244 {
245     shutdown();
246     delete m_lock;
247 }
248
249 void ReloadableXMLFile::startup()
250 {
251     if (m_lock && !m_reload_thread) {
252         m_reload_wait = CondWait::create();
253         m_reload_thread = Thread::create(&reload_fn, this);
254     }
255 }
256
257 void ReloadableXMLFile::shutdown()
258 {
259     if (m_reload_thread) {
260         // Shut down the reload thread and let it know.
261         m_shutdown = true;
262         m_reload_wait->signal();
263         m_reload_thread->join(nullptr);
264         delete m_reload_thread;
265         delete m_reload_wait;
266         m_reload_thread = nullptr;
267         m_reload_wait = nullptr;
268     }
269 }
270
271 void* ReloadableXMLFile::reload_fn(void* pv)
272 {
273     ReloadableXMLFile* r = reinterpret_cast<ReloadableXMLFile*>(pv);
274
275 #ifndef WIN32
276     // First, let's block all signals
277     Thread::mask_all_signals();
278 #endif
279
280     if (!r->m_id.empty()) {
281         string threadid("[");
282         threadid += r->m_id + ']';
283         logging::NDC::push(threadid);
284     }
285
286 #ifdef _DEBUG
287     NDC ndc("reload");
288 #endif
289
290     auto_ptr<Mutex> mutex(Mutex::create());
291     mutex->lock();
292
293     if (r->m_local)
294         r->m_log.info("reload thread started...running when signaled");
295     else
296         r->m_log.info("reload thread started...running every %d seconds", r->m_reloadInterval);
297
298     while (!r->m_shutdown) {
299         if (r->m_local)
300             r->m_reload_wait->wait(mutex.get());
301         else
302             r->m_reload_wait->timedwait(mutex.get(), r->m_reloadInterval);
303         if (r->m_shutdown)
304             break;
305
306         try {
307             r->m_log.info("reloading %s resource...", r->m_local ? "local" : "remote");
308             pair<bool,DOMElement*> ret = r->background_load();
309             if (ret.first)
310                 ret.second->getOwnerDocument()->release();
311         }
312         catch (long& ex) {
313             if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
314                 r->m_log.info("remote resource (%s) unchanged from cached version", r->m_source.c_str());
315             }
316             else {
317                 // Shouldn't happen, we should only get codes intended to be gracefully handled.
318                 r->m_log.crit("maintaining existing configuration, remote resource fetch returned atypical status code (%d)", ex);
319             }
320         }
321         catch (exception& ex) {
322             r->m_log.crit("maintaining existing configuration, error reloading resource (%s): %s", r->m_source.c_str(), ex.what());
323         }
324     }
325
326     r->m_log.info("reload thread finished");
327
328     mutex->unlock();
329
330     if (!r->m_id.empty()) {
331         logging::NDC::pop();
332     }
333
334     return nullptr;
335 }
336
337 Lockable* ReloadableXMLFile::lock()
338 {
339     if (!m_lock)
340         return this;
341
342     m_lock->rdlock();
343
344     if (m_local) {
345     // Check if we need to refresh.
346 #ifdef WIN32
347         struct _stat stat_buf;
348         if (_stat(m_source.c_str(), &stat_buf) != 0)
349             return this;
350 #else
351         struct stat stat_buf;
352         if (stat(m_source.c_str(), &stat_buf) != 0)
353             return this;
354 #endif
355         if (m_filestamp >= stat_buf.st_mtime)
356             return this;
357
358         // Elevate lock and recheck.
359         m_log.debug("timestamp of local resource changed, elevating to a write lock");
360         m_lock->unlock();
361         m_lock->wrlock();
362         if (m_filestamp >= stat_buf.st_mtime) {
363             // Somebody else handled it, just downgrade.
364             m_log.debug("update of local resource handled by another thread, downgrading lock");
365             m_lock->unlock();
366             m_lock->rdlock();
367             return this;
368         }
369
370         // Update the timestamp regardless.
371         m_filestamp = stat_buf.st_mtime;
372         if (m_reload_wait) {
373             m_log.info("change detected, signaling reload thread...");
374             m_reload_wait->signal();
375         }
376         else {
377             m_log.warn("change detected, but reload thread not started");
378         }
379     }
380
381     return this;
382 }
383
384 void ReloadableXMLFile::unlock()
385 {
386     if (m_lock)
387         m_lock->unlock();
388 }
389
390 pair<bool,DOMElement*> ReloadableXMLFile::load(bool backup)
391 {
392 #ifdef _DEBUG
393     NDC ndc("load");
394 #endif
395
396     try {
397         if (m_source.empty()) {
398             // Data comes from the DOM we were handed.
399             m_log.debug("loading inline configuration...");
400             return make_pair(false, XMLHelper::getFirstChildElement(m_root));
401         }
402         else {
403             // Data comes from a file we have to parse.
404             if (backup)
405                 m_log.info("using local backup of remote resource");
406             else
407                 m_log.debug("loading configuration from external resource...");
408
409             DOMDocument* doc=nullptr;
410             if (m_local || backup) {
411                 auto_ptr_XMLCh widenit(backup ? m_backing.c_str() : m_source.c_str());
412                 // Use library-wide lock for now, nothing else is using it anyway.
413                 Locker locker(backup ? getBackupLock() : nullptr);
414                 LocalFileInputSource src(widenit.get());
415                 Wrapper4InputSource dsrc(&src, false);
416                 if (m_validate)
417                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
418                 else
419                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
420             }
421             else {
422                 URLInputSource src(m_root, nullptr, &m_cacheTag);
423                 Wrapper4InputSource dsrc(&src, false);
424                 if (m_validate)
425                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
426                 else
427                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
428
429                 // Check for a response code signal.
430                 if (XMLHelper::isNodeNamed(doc->getDocumentElement(), xmlconstants::XMLTOOLING_NS, URLInputSource::utf16StatusCodeElementName)) {
431                     int responseCode = XMLString::parseInt(doc->getDocumentElement()->getFirstChild()->getNodeValue());
432                     doc->release();
433                     if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED)
434                         throw (long)responseCode; // toss out as a "known" case to handle gracefully
435                     else {
436                         m_log.warn("remote resource fetch returned atypical status code (%d)", responseCode);
437                         throw IOException("remote resource fetch failed, check log for status code of response");
438                     }
439                 }
440             }
441
442             m_log.infoStream() << "loaded XML resource (" << (backup ? m_backing : m_source) << ")" << logging::eol;
443 #ifndef XMLTOOLING_LITE
444             if (m_credResolver || m_trust) {
445                 m_log.debug("checking signature on XML resource");
446                 try {
447                     DOMElement* sigel = XMLHelper::getFirstChildElement(doc->getDocumentElement(), xmlconstants::XMLSIG_NS, Signature::LOCAL_NAME);
448                     if (!sigel)
449                         throw XMLSecurityException("Signature validation required, but no signature found.");
450
451                     // Wrap and unmarshall the signature for the duration of the check.
452                     auto_ptr<Signature> sigobj(dynamic_cast<Signature*>(SignatureBuilder::buildOneFromElement(sigel)));    // don't bind to document
453                     validateSignature(*sigobj.get());
454                 }
455                 catch (exception&) {
456                     doc->release();
457                     throw;
458                 }
459
460             }
461 #endif
462             return make_pair(true, doc->getDocumentElement());
463         }
464     }
465     catch (XMLException& e) {
466         auto_ptr_char msg(e.getMessage());
467         m_log.errorStream() << "Xerces error while loading resource (" << (backup ? m_backing : m_source) << "): "
468             << msg.get() << logging::eol;
469         throw XMLParserException(msg.get());
470     }
471     catch (exception& e) {
472         m_log.errorStream() << "error while loading resource ("
473             << (m_source.empty() ? "inline" : (backup ? m_backing : m_source)) << "): " << e.what() << logging::eol;
474         throw;
475     }
476 }
477
478 pair<bool,DOMElement*> ReloadableXMLFile::load()
479 {
480     // If this method is used, we're responsible for managing failover to a
481     // backup of a remote resource (if available), and for backing up remote
482     // resources.
483     try {
484         pair<bool,DOMElement*> ret = load(false);
485         if (!m_backing.empty()) {
486             m_log.debug("backing up remote resource to (%s)", m_backing.c_str());
487             try {
488                 Locker locker(getBackupLock());
489                 ofstream backer(m_backing.c_str());
490                 backer << *(ret.second->getOwnerDocument());
491                 preserveCacheTag();
492             }
493             catch (exception& ex) {
494                 m_log.crit("exception while backing up resource: %s", ex.what());
495             }
496         }
497         return ret;
498     }
499     catch (long& responseCode) {
500         // If there's an HTTP error or the document hasn't changed,
501         // use the backup iff we have no "valid" resource in place.
502         // That prevents reload of the backup copy any time the document
503         // hasn't changed.
504         if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED)
505             m_log.info("remote resource (%s) unchanged from cached version", m_source.c_str());
506         if (!m_loaded && !m_backing.empty())
507             return load(true);
508         throw;
509     }
510     catch (exception&) {
511         // Same as above, but for general load/parse errors.
512         if (!m_loaded && !m_backing.empty())
513             return load(true);
514         throw;
515     }
516 }
517
518 pair<bool,DOMElement*> ReloadableXMLFile::background_load()
519 {
520     // If this method isn't overridden, we acquire a write lock
521     // and just call the old override.
522     if (m_lock)
523         m_lock->wrlock();
524     SharedLock locker(m_lock, false);
525     return load();
526 }
527
528 Lockable* ReloadableXMLFile::getBackupLock()
529 {
530     return &XMLToolingConfig::getConfig();
531 }
532
533 void ReloadableXMLFile::preserveCacheTag()
534 {
535     if (!m_cacheTag.empty() && !m_backing.empty()) {
536         try {
537             string tagname = m_backing + ".tag";
538             ofstream backer(tagname.c_str());
539             backer << m_cacheTag;
540         }
541         catch (exception&) {
542         }
543     }
544 }
545
546 #ifndef XMLTOOLING_LITE
547
548 void ReloadableXMLFile::validateSignature(Signature& sigObj) const
549 {
550     DSIGSignature* sig=sigObj.getXMLSignature();
551     if (!sig)
552         throw XMLSecurityException("Signature does not exist yet.");
553
554     // Make sure the whole document was signed.
555     bool valid=false;
556     DSIGReferenceList* refs=sig->getReferenceList();
557     if (refs && refs->getSize()==1) {
558         DSIGReference* ref=refs->item(0);
559         if (ref) {
560             const XMLCh* URI=ref->getURI();
561             if (URI==nullptr || *URI==0) {
562                 DSIGTransformList* tlist=ref->getTransforms();
563                 if (tlist->getSize() <= 2) { 
564                     for (unsigned int i=0; tlist && i<tlist->getSize(); i++) {
565                         if (tlist->item(i)->getTransformType()==TRANSFORM_ENVELOPED_SIGNATURE)
566                             valid=true;
567                         else if (tlist->item(i)->getTransformType()!=TRANSFORM_EXC_C14N &&
568                                  tlist->item(i)->getTransformType()!=TRANSFORM_C14N
569 #ifdef XMLTOOLING_XMLSEC_C14N11
570                                  && tlist->item(i)->getTransformType()!=TRANSFORM_C14N11
571 #endif
572                                  ) {
573                             valid=false;
574                             break;
575                         }
576                     }
577                 }
578             }
579         }
580     }
581     
582     if (!valid)
583         throw XMLSecurityException("Invalid signature profile for signed configuration resource.");
584
585     // Set up criteria.
586     CredentialCriteria cc;
587     cc.setUsage(Credential::SIGNING_CREDENTIAL);
588     cc.setSignature(sigObj, CredentialCriteria::KEYINFO_EXTRACTION_KEY);
589     if (!m_signerName.empty())
590         cc.setPeerName(m_signerName.c_str());
591
592     if (m_credResolver) {
593         Locker locker(m_credResolver);
594         vector<const Credential*> creds;
595         if (m_credResolver->resolve(creds, &cc)) {
596             SignatureValidator sigValidator;
597             for (vector<const Credential*>::const_iterator i = creds.begin(); i != creds.end(); ++i) {
598                 try {
599                     sigValidator.setCredential(*i);
600                     sigValidator.validate(&sigObj);
601                     return; // success!
602                 }
603                 catch (exception&) {
604                 }
605             }
606             throw XMLSecurityException("Unable to verify signature with supplied key(s).");
607         }
608         else {
609             throw XMLSecurityException("CredentialResolver did not supply any candidate keys.");
610         }
611     }
612     else if (m_trust) {
613         DummyCredentialResolver dummy;
614         if (m_trust->validate(sigObj, dummy, &cc))
615             return;
616         throw XMLSecurityException("TrustEngine unable to verify signature.");
617     }
618
619     throw XMLSecurityException("Unable to verify signature.");
620 }
621
622 #endif