https://bugs.internet2.edu/jira/browse/CPPXT-58
[shibboleth/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 backingFilePath[] =  UNICODE_LITERAL_15(b,a,c,k,i,n,g,F,i,l,e,P,a,t,h);
96 static const XMLCh type[] =             UNICODE_LITERAL_4(t,y,p,e);
97 static const XMLCh certificate[] =      UNICODE_LITERAL_11(c,e,r,t,i,f,i,c,a,t,e);
98 static const XMLCh signerName[] =       UNICODE_LITERAL_10(s,i,g,n,e,r,N,a,m,e);
99 static const XMLCh _TrustEngine[] =     UNICODE_LITERAL_11(T,r,u,s,t,E,n,g,i,n,e);
100 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);
101
102
103 ReloadableXMLFile::ReloadableXMLFile(const DOMElement* e, Category& log)
104     : m_root(e), m_local(true), m_validate(false), m_backupIndicator(true), m_filestamp(0), m_reloadInterval(0), m_lock(nullptr), m_log(log),
105 #ifndef XMLTOOLING_LITE
106         m_credResolver(nullptr), m_trust(nullptr),
107 #endif
108         m_shutdown(false), m_reload_wait(nullptr), m_reload_thread(nullptr)
109 {
110 #ifdef _DEBUG
111     NDC ndc("ReloadableXMLFile");
112 #endif
113
114     // Establish source of data...
115     const XMLCh* source=e->getAttributeNS(nullptr,uri);
116     if (!source || !*source) {
117         source=e->getAttributeNS(nullptr,url);
118         if (!source || !*source) {
119             source=e->getAttributeNS(nullptr,path);
120             if (!source || !*source) {
121                 source=e->getAttributeNS(nullptr,pathname);
122                 if (!source || !*source) {
123                     source=e->getAttributeNS(nullptr,file);
124                     if (!source || !*source) {
125                         source=e->getAttributeNS(nullptr,filename);
126                     }
127                 }
128             }
129         }
130         else {
131             m_local=false;
132         }
133     }
134     else {
135         m_local=false;
136     }
137
138     if (source && *source) {
139         const XMLCh* flag=e->getAttributeNS(nullptr,validate);
140         m_validate=(XMLString::equals(flag,xmlconstants::XML_TRUE) || XMLString::equals(flag,xmlconstants::XML_ONE));
141
142         auto_ptr_char temp(source);
143         m_source=temp.get();
144
145         if (!m_local && !strstr(m_source.c_str(),"://")) {
146             log.warn("deprecated usage of uri/url attribute for a local resource, use path instead");
147             m_local=true;
148         }
149
150 #ifndef XMLTOOLING_LITE
151         // Check for signature bits.
152         if (e && e->hasAttributeNS(nullptr, certificate)) {
153             // Use a file-based credential resolver rooted here.
154             m_credResolver = XMLToolingConfig::getConfig().CredentialResolverManager.newPlugin(FILESYSTEM_CREDENTIAL_RESOLVER, e);
155         }
156         else {
157             const DOMElement* sub = e ? XMLHelper::getFirstChildElement(e, _CredentialResolver) : nullptr;
158             auto_ptr_char t(sub ? sub->getAttributeNS(nullptr, type) : nullptr);
159             if (t.get()) {
160                 m_credResolver = XMLToolingConfig::getConfig().CredentialResolverManager.newPlugin(t.get(), sub);
161             }
162             else {
163                 sub = e ? XMLHelper::getFirstChildElement(e, _TrustEngine) : nullptr;
164                 auto_ptr_char t2(sub ? sub->getAttributeNS(nullptr, type) : nullptr);
165                 if (t2.get()) {
166                     TrustEngine* trust = XMLToolingConfig::getConfig().TrustEngineManager.newPlugin(t2.get(), sub);
167                     if (!(m_trust = dynamic_cast<SignatureTrustEngine*>(trust))) {
168                         delete trust;
169                         throw XMLToolingException("TrustEngine-based ReloadableXMLFile requires a SignatureTrustEngine plugin.");
170                     }
171
172                     flag = e->getAttributeNS(nullptr, signerName);
173                     if (flag && *flag) {
174                         auto_ptr_char sn(flag);
175                         m_signerName = sn.get();
176                     }
177                 }
178             }
179         }
180 #endif
181
182         if (m_local) {
183             XMLToolingConfig::getConfig().getPathResolver()->resolve(m_source, PathResolver::XMLTOOLING_CFG_FILE);
184
185             flag=e->getAttributeNS(nullptr,reloadChanges);
186             if (!XMLString::equals(flag,xmlconstants::XML_FALSE) && !XMLString::equals(flag,xmlconstants::XML_ZERO)) {
187 #ifdef WIN32
188                 struct _stat stat_buf;
189                 if (_stat(m_source.c_str(), &stat_buf) == 0)
190 #else
191                 struct stat stat_buf;
192                 if (stat(m_source.c_str(), &stat_buf) == 0)
193 #endif
194                     m_filestamp=stat_buf.st_mtime;
195                 else
196                     throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
197                 m_lock=RWLock::create();
198             }
199             log.debug("using local resource (%s), will %smonitor for changes", m_source.c_str(), m_lock ? "" : "not ");
200         }
201         else {
202             log.debug("using remote resource (%s)", m_source.c_str());
203             source = e->getAttributeNS(nullptr,backingFilePath);
204             if (source && *source) {
205                 auto_ptr_char temp2(source);
206                 m_backing=temp2.get();
207                 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_backing, PathResolver::XMLTOOLING_RUN_FILE);
208                 log.debug("backup remote resource to (%s)", m_backing.c_str());
209             }
210             source = e->getAttributeNS(nullptr,reloadInterval);
211             if (source && *source) {
212                 m_reloadInterval = XMLString::parseInt(source);
213                 if (m_reloadInterval > 0) {
214                     m_log.debug("will reload remote resource at most every %d seconds", m_reloadInterval);
215                     m_lock=RWLock::create();
216                 }
217             }
218             m_filestamp = time(nullptr);   // assume it gets loaded initially
219         }
220
221         if (m_lock) {
222             m_reload_wait = CondWait::create();
223             m_reload_thread = Thread::create(&reload_fn, this);
224         }
225     }
226     else {
227         log.debug("no resource uri/path/name supplied, will load inline configuration");
228     }
229
230     source = e->getAttributeNS(nullptr, id);
231     if (source && *source) {
232         auto_ptr_char tempid(source);
233         m_id = tempid.get();
234     }
235 }
236
237 ReloadableXMLFile::~ReloadableXMLFile()
238 {
239     shutdown();
240     delete m_lock;
241 }
242
243 void ReloadableXMLFile::shutdown()
244 {
245     if (m_reload_thread) {
246         // Shut down the reload thread and let it know.
247         m_shutdown = true;
248         m_reload_wait->signal();
249         m_reload_thread->join(nullptr);
250         delete m_reload_thread;
251         delete m_reload_wait;
252         m_reload_thread = nullptr;
253         m_reload_wait = nullptr;
254     }
255 }
256
257 void* ReloadableXMLFile::reload_fn(void* pv)
258 {
259     ReloadableXMLFile* r = reinterpret_cast<ReloadableXMLFile*>(pv);
260
261 #ifndef WIN32
262     // First, let's block all signals
263     Thread::mask_all_signals();
264 #endif
265
266     if (!r->m_id.empty()) {
267         string threadid("[");
268         threadid += r->m_id + ']';
269         logging::NDC::push(threadid);
270     }
271
272 #ifdef _DEBUG
273     NDC ndc("reload");
274 #endif
275
276     auto_ptr<Mutex> mutex(Mutex::create());
277     mutex->lock();
278
279     if (r->m_local)
280         r->m_log.info("reload thread started...running when signaled");
281     else
282         r->m_log.info("reload thread started...running every %d seconds", r->m_reloadInterval);
283
284     while (!r->m_shutdown) {
285         if (r->m_local)
286             r->m_reload_wait->wait(mutex.get());
287         else
288             r->m_reload_wait->timedwait(mutex.get(), r->m_reloadInterval);
289         if (r->m_shutdown)
290             break;
291
292         try {
293             r->m_log.info("reloading %s resource...", r->m_local ? "local" : "remote");
294             pair<bool,DOMElement*> ret = r->background_load();
295             if (ret.first)
296                 ret.second->getOwnerDocument()->release();
297         }
298         catch (long& ex) {
299             if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
300                 r->m_log.info("remote resource (%s) unchanged from cached version", r->m_source.c_str());
301             }
302             else {
303                 // Shouldn't happen, we should only get codes intended to be gracefully handled.
304                 r->m_log.crit("maintaining existing configuration, remote resource fetch returned atypical status code (%d)", ex);
305             }
306         }
307         catch (exception& ex) {
308             r->m_log.crit("maintaining existing configuration, error reloading resource (%s): %s", r->m_source.c_str(), ex.what());
309         }
310     }
311
312     r->m_log.info("reload thread finished");
313
314     mutex->unlock();
315
316     if (!r->m_id.empty()) {
317         logging::NDC::pop();
318     }
319
320     return nullptr;
321 }
322
323 Lockable* ReloadableXMLFile::lock()
324 {
325     if (!m_lock)
326         return this;
327
328     m_lock->rdlock();
329
330     if (m_local) {
331     // Check if we need to refresh.
332 #ifdef WIN32
333         struct _stat stat_buf;
334         if (_stat(m_source.c_str(), &stat_buf) != 0)
335             return this;
336 #else
337         struct stat stat_buf;
338         if (stat(m_source.c_str(), &stat_buf) != 0)
339             return this;
340 #endif
341         if (m_filestamp >= stat_buf.st_mtime)
342             return this;
343
344         // Elevate lock and recheck.
345         m_log.debug("timestamp of local resource changed, elevating to a write lock");
346         m_lock->unlock();
347         m_lock->wrlock();
348         if (m_filestamp >= stat_buf.st_mtime) {
349             // Somebody else handled it, just downgrade.
350             m_log.debug("update of local resource handled by another thread, downgrading lock");
351             m_lock->unlock();
352             m_lock->rdlock();
353             return this;
354         }
355
356         // Update the timestamp regardless.
357         m_filestamp = stat_buf.st_mtime;
358         m_log.info("change detected, signaling reload thread...");
359         m_reload_wait->signal();
360     }
361
362     return this;
363 }
364
365 void ReloadableXMLFile::unlock()
366 {
367     if (m_lock)
368         m_lock->unlock();
369 }
370
371 pair<bool,DOMElement*> ReloadableXMLFile::load(bool backup)
372 {
373 #ifdef _DEBUG
374     NDC ndc("load");
375 #endif
376
377     try {
378         if (m_source.empty()) {
379             // Data comes from the DOM we were handed.
380             m_log.debug("loading inline configuration...");
381             return make_pair(false, XMLHelper::getFirstChildElement(m_root));
382         }
383         else {
384             // Data comes from a file we have to parse.
385             if (backup)
386                 m_log.warn("using local backup of remote resource");
387             else
388                 m_log.debug("loading configuration from external resource...");
389
390             DOMDocument* doc=nullptr;
391             if (m_local || backup) {
392                 auto_ptr_XMLCh widenit(backup ? m_backing.c_str() : m_source.c_str());
393                 // Use library-wide lock for now, nothing else is using it anyway.
394                 Locker locker(backup ? getBackupLock() : nullptr);
395                 LocalFileInputSource src(widenit.get());
396                 Wrapper4InputSource dsrc(&src, false);
397                 if (m_validate)
398                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
399                 else
400                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
401             }
402             else {
403                 URLInputSource src(m_root, nullptr, &m_cacheTag);
404                 Wrapper4InputSource dsrc(&src, false);
405                 if (m_validate)
406                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
407                 else
408                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
409
410                 // Check for a response code signal.
411                 if (XMLHelper::isNodeNamed(doc->getDocumentElement(), xmlconstants::XMLTOOLING_NS, URLInputSource::utf16StatusCodeElementName)) {
412                     int responseCode = XMLString::parseInt(doc->getDocumentElement()->getFirstChild()->getNodeValue());
413                     doc->release();
414                     if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
415                         throw (long)responseCode; // toss out as a "known" case to handle gracefully
416                     }
417                     else {
418                         m_log.warn("remote resource fetch returned atypical status code (%d)", responseCode);
419                         throw IOException("remote resource fetch failed, check log for status code of response");
420                     }
421                 }
422             }
423
424             m_log.infoStream() << "loaded XML resource (" << (backup ? m_backing : m_source) << ")" << logging::eol;
425 #ifndef XMLTOOLING_LITE
426             if (m_credResolver || m_trust) {
427                 m_log.debug("checking signature on XML resource");
428                 try {
429                     DOMElement* sigel = XMLHelper::getFirstChildElement(doc->getDocumentElement(), xmlconstants::XMLSIG_NS, Signature::LOCAL_NAME);
430                     if (!sigel)
431                         throw XMLSecurityException("Signature validation required, but no signature found.");
432
433                     // Wrap and unmarshall the signature for the duration of the check.
434                     auto_ptr<Signature> sigobj(dynamic_cast<Signature*>(SignatureBuilder::buildOneFromElement(sigel)));    // don't bind to document
435                     validateSignature(*sigobj.get());
436                 }
437                 catch (exception&) {
438                     doc->release();
439                     throw;
440                 }
441
442             }
443 #endif
444
445             if (!backup && !m_backing.empty()) {
446                 // If the indicator is true, we're responsible for the backup.
447                 if (m_backupIndicator) {
448                     m_log.debug("backing up remote resource to (%s)", m_backing.c_str());
449                     try {
450                         Locker locker(getBackupLock());
451                         ofstream backer(m_backing.c_str());
452                         backer << *doc;
453                     }
454                     catch (exception& ex) {
455                         m_log.crit("exception while backing up resource: %s", ex.what());
456                     }
457                 }
458                 else {
459                     // If the indicator was false, set true to signal that a backup is needed.
460                     // The caller will presumably flip it back to false once that's done.
461                     m_backupIndicator = true;
462                 }
463             }
464
465             return make_pair(true, doc->getDocumentElement());
466         }
467     }
468     catch (XMLException& e) {
469         auto_ptr_char msg(e.getMessage());
470         m_log.errorStream() << "Xerces error while loading resource (" << (backup ? m_backing : m_source) << "): "
471             << msg.get() << logging::eol;
472         if (!backup && !m_backing.empty())
473             return load(true);
474         throw XMLParserException(msg.get());
475     }
476     catch (exception& e) {
477         m_log.errorStream() << "error while loading resource ("
478             << (m_source.empty() ? "inline" : (backup ? m_backing : m_source)) << "): " << e.what() << logging::eol;
479         if (!backup && !m_backing.empty())
480             return load(true);
481         throw;
482     }
483 }
484
485 #ifndef XMLTOOLING_LITE
486
487 void ReloadableXMLFile::validateSignature(Signature& sigObj) const
488 {
489     DSIGSignature* sig=sigObj.getXMLSignature();
490     if (!sig)
491         throw XMLSecurityException("Signature does not exist yet.");
492
493     // Make sure the whole document was signed.
494     bool valid=false;
495     DSIGReferenceList* refs=sig->getReferenceList();
496     if (refs && refs->getSize()==1) {
497         DSIGReference* ref=refs->item(0);
498         if (ref) {
499             const XMLCh* URI=ref->getURI();
500             if (URI==nullptr || *URI==0) {
501                 DSIGTransformList* tlist=ref->getTransforms();
502                 if (tlist->getSize() <= 2) { 
503                     for (unsigned int i=0; tlist && i<tlist->getSize(); i++) {
504                         if (tlist->item(i)->getTransformType()==TRANSFORM_ENVELOPED_SIGNATURE)
505                             valid=true;
506                         else if (tlist->item(i)->getTransformType()!=TRANSFORM_EXC_C14N &&
507                                  tlist->item(i)->getTransformType()!=TRANSFORM_C14N &&
508                                  tlist->item(i)->getTransformType()!=TRANSFORM_C14N11) {
509                             valid=false;
510                             break;
511                         }
512                     }
513                 }
514             }
515         }
516     }
517     
518     if (!valid)
519         throw XMLSecurityException("Invalid signature profile for signed configuration resource.");
520
521     // Set up criteria.
522     CredentialCriteria cc;
523     cc.setUsage(Credential::SIGNING_CREDENTIAL);
524     cc.setSignature(sigObj, CredentialCriteria::KEYINFO_EXTRACTION_KEY);
525     if (!m_signerName.empty())
526         cc.setPeerName(m_signerName.c_str());
527
528     if (m_credResolver) {
529         Locker locker(m_credResolver);
530         vector<const Credential*> creds;
531         if (m_credResolver->resolve(creds, &cc)) {
532             SignatureValidator sigValidator;
533             for (vector<const Credential*>::const_iterator i = creds.begin(); i != creds.end(); ++i) {
534                 try {
535                     sigValidator.setCredential(*i);
536                     sigValidator.validate(&sigObj);
537                     return; // success!
538                 }
539                 catch (exception&) {
540                 }
541             }
542             throw XMLSecurityException("Unable to verify signature with supplied key(s).");
543         }
544         else {
545             throw XMLSecurityException("CredentialResolver did not supply any candidate keys.");
546         }
547     }
548     else if (m_trust) {
549         DummyCredentialResolver dummy;
550         if (m_trust->validate(sigObj, dummy, &cc))
551             return;
552         throw XMLSecurityException("TrustEngine unable to verify signature.");
553     }
554
555     throw XMLSecurityException("Unable to verify signature.");
556 }
557
558 #endif
559
560 pair<bool,DOMElement*> ReloadableXMLFile::load()
561 {
562     return load(false);
563 }
564
565 pair<bool,DOMElement*> ReloadableXMLFile::background_load()
566 {
567     // If this method isn't overridden, we acquire a write lock
568     // and just call the old override.
569     if (m_lock)
570         m_lock->wrlock();
571     SharedLock locker(m_lock, false);
572     return load();
573 }
574
575 Lockable* ReloadableXMLFile::getBackupLock()
576 {
577     return &XMLToolingConfig::getConfig();
578 }