25e4324a7326a9a010ceb7097c08cbaaa4b82fc2
[shibboleth/cpp-xmltooling.git] / xmltooling / util / ParserPool.cpp
1 /*
2  *  Copyright 2001-2008 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  * ParserPool.cpp
19  *
20  * A thread-safe pool of parsers that share characteristics.
21  */
22
23 #include "internal.h"
24 #include "exceptions.h"
25 #include "logging.h"
26 #include "util/CurlURLInputStream.h"
27 #include "util/NDC.h"
28 #include "util/ParserPool.h"
29 #include "util/XMLHelper.h"
30
31 #include <algorithm>
32 #include <functional>
33 #include <sys/types.h>
34 #include <sys/stat.h>
35 #include <xercesc/util/PlatformUtils.hpp>
36 #include <xercesc/util/XMLUniDefs.hpp>
37 #include <xercesc/sax/SAXException.hpp>
38 #include <xercesc/framework/MemBufInputSource.hpp>
39 #include <xercesc/framework/LocalFileInputSource.hpp>
40 #include <xercesc/framework/Wrapper4InputSource.hpp>
41
42 using namespace xmltooling::logging;
43 using namespace xmltooling;
44 using namespace xercesc;
45 using namespace std;
46
47
48 namespace {
49     class MyErrorHandler : public DOMErrorHandler {
50     public:
51         unsigned int errors;
52
53         MyErrorHandler() : errors(0) {}
54
55         bool handleError(const DOMError& e)
56         {
57 #ifdef _DEBUG
58             xmltooling::NDC ndc("handleError");
59 #endif
60             Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
61
62             DOMLocator* locator=e.getLocation();
63             auto_ptr_char temp(e.getMessage());
64
65             switch (e.getSeverity()) {
66                 case DOMError::DOM_SEVERITY_WARNING:
67                     log.warnStream() << "warning on line " << locator->getLineNumber()
68                         << ", column " << locator->getColumnNumber()
69                         << ", message: " << temp.get() << logging::eol;
70                     return true;
71
72                 case DOMError::DOM_SEVERITY_ERROR:
73                     ++errors;
74                     log.errorStream() << "error on line " << locator->getLineNumber()
75                         << ", column " << locator->getColumnNumber()
76                         << ", message: " << temp.get() << logging::eol;
77                     return true;
78
79                 case DOMError::DOM_SEVERITY_FATAL_ERROR:
80                     ++errors;
81                     log.errorStream() << "fatal error on line " << locator->getLineNumber()
82                         << ", column " << locator->getColumnNumber()
83                         << ", message: " << temp.get() << logging::eol;
84                     return true;
85             }
86
87             ++errors;
88             log.errorStream() << "undefined error type on line " << locator->getLineNumber()
89                 << ", column " << locator->getColumnNumber()
90                 << ", message: " << temp.get() << logging::eol;
91             return false;
92         }
93     };
94 }
95
96
97 ParserPool::ParserPool(bool namespaceAware, bool schemaAware)
98     : m_namespaceAware(namespaceAware), m_schemaAware(schemaAware), m_lock(Mutex::create()), m_security(new SecurityManager()) {}
99
100 ParserPool::~ParserPool()
101 {
102     while(!m_pool.empty()) {
103         m_pool.top()->release();
104         m_pool.pop();
105     }
106     delete m_lock;
107     delete m_security;
108 }
109
110 DOMDocument* ParserPool::newDocument()
111 {
112     return DOMImplementationRegistry::getDOMImplementation(NULL)->createDocument();
113 }
114
115 #ifdef XMLTOOLING_XERCESC_COMPLIANT_DOMLS
116
117 DOMDocument* ParserPool::parse(DOMLSInput& domsrc)
118 {
119     DOMLSParser* parser=checkoutBuilder();
120     XercesJanitor<DOMLSParser> janitor(parser);
121     try {
122         MyErrorHandler deh;
123         parser->getDomConfig()->setParameter(XMLUni::fgDOMErrorHandler, dynamic_cast<DOMErrorHandler*>(&deh));
124         DOMDocument* doc=parser->parse(&domsrc);
125         if (deh.errors) {
126             if (doc)
127                 doc->release();
128             throw XMLParserException("XML error(s) during parsing, check log for specifics");
129         }
130         parser->getDomConfig()->setParameter(XMLUni::fgDOMErrorHandler, (void*)NULL);
131         parser->getDomConfig()->setParameter(XMLUni::fgXercesUserAdoptsDOMDocument, true);
132         checkinBuilder(janitor.release());
133         return doc;
134     }
135     catch (XMLException& ex) {
136         parser->getDomConfig()->setParameter(XMLUni::fgDOMErrorHandler, (void*)NULL);
137         parser->getDomConfig()->setParameter(XMLUni::fgXercesUserAdoptsDOMDocument, true);
138         checkinBuilder(janitor.release());
139         auto_ptr_char temp(ex.getMessage());
140         throw XMLParserException(string("Xerces error during parsing: ") + (temp.get() ? temp.get() : "no message"));
141     }
142     catch (XMLToolingException&) {
143         parser->getDomConfig()->setParameter(XMLUni::fgDOMErrorHandler, (void*)NULL);
144         parser->getDomConfig()->setParameter(XMLUni::fgXercesUserAdoptsDOMDocument, true);
145         checkinBuilder(janitor.release());
146         throw;
147     }
148 }
149
150 #else
151
152 DOMDocument* ParserPool::parse(DOMInputSource& domsrc)
153 {
154     DOMBuilder* parser=checkoutBuilder();
155     XercesJanitor<DOMBuilder> janitor(parser);
156     try {
157         MyErrorHandler deh;
158         parser->setErrorHandler(&deh);
159         DOMDocument* doc=parser->parse(domsrc);
160         if (deh.errors) {
161             if (doc)
162                 doc->release();
163             throw XMLParserException("XML error(s) during parsing, check log for specifics");
164         }
165         parser->setErrorHandler(NULL);
166         parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument, true);
167         checkinBuilder(janitor.release());
168         return doc;
169     }
170     catch (XMLException& ex) {
171         parser->setErrorHandler(NULL);
172         parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument, true);
173         checkinBuilder(janitor.release());
174         auto_ptr_char temp(ex.getMessage());
175         throw XMLParserException(string("Xerces error during parsing: ") + (temp.get() ? temp.get() : "no message"));
176     }
177     catch (XMLToolingException&) {
178         parser->setErrorHandler(NULL);
179         parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument, true);
180         checkinBuilder(janitor.release());
181         throw;
182     }
183 }
184
185 #endif
186
187 DOMDocument* ParserPool::parse(istream& is)
188 {
189     StreamInputSource src(is);
190     Wrapper4InputSource domsrc(&src,false);
191     return parse(domsrc);
192 }
193
194 // Functor to double its argument separated by a character and append to a buffer
195 template <class T> class doubleit
196 {
197 public:
198     doubleit(T& t, const typename T::value_type& s) : temp(t), sep(s) {}
199     void operator() (const pair<const T,T>& s) { temp += s.first + sep + s.first + sep; }
200     T& temp;
201     const typename T::value_type& sep;
202 };
203
204 bool ParserPool::loadSchema(const XMLCh* nsURI, const XMLCh* pathname)
205 {
206     // Just check the pathname and then directly register the pair into the map.
207
208     auto_ptr_char p(pathname);
209 #ifdef WIN32
210     struct _stat stat_buf;
211     if (_stat(p.get(), &stat_buf) != 0)
212 #else
213     struct stat stat_buf;
214     if (stat(p.get(), &stat_buf) != 0)
215 #endif
216     {
217 #if _DEBUG
218         xmltooling::NDC ndc("loadSchema");
219 #endif
220         Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
221         auto_ptr_char n(nsURI);
222         log.error("failed to load schema for (%s), file not found (%s)",n.get(),p.get());
223         return false;
224     }
225
226     Lock lock(m_lock);
227     m_schemaLocMap[nsURI]=pathname;
228     m_schemaLocations.erase();
229     for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
230
231     return true;
232 }
233
234 bool ParserPool::loadCatalog(const XMLCh* pathname)
235 {
236 #if _DEBUG
237     xmltooling::NDC ndc("loadCatalog");
238 #endif
239     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
240
241     // XML constants
242     static const XMLCh catalog[] =  UNICODE_LITERAL_7(c,a,t,a,l,o,g);
243     static const XMLCh system[] =   UNICODE_LITERAL_6(s,y,s,t,e,m);
244     static const XMLCh systemId[] = UNICODE_LITERAL_8(s,y,s,t,e,m,I,d);
245     static const XMLCh uri[] =      UNICODE_LITERAL_3(u,r,i);
246     static const XMLCh CATALOG_NS[] = {
247         chLatin_u, chLatin_r, chLatin_n, chColon,
248         chLatin_o, chLatin_a, chLatin_s, chLatin_i, chLatin_s, chColon,
249         chLatin_n, chLatin_a, chLatin_m, chLatin_e, chLatin_s, chColon,
250         chLatin_t, chLatin_c, chColon,
251         chLatin_e, chLatin_n, chLatin_t, chLatin_i, chLatin_t, chLatin_y, chColon,
252         chLatin_x, chLatin_m, chLatin_l, chLatin_n, chLatin_s, chColon,
253         chLatin_x, chLatin_m, chLatin_l, chColon,
254         chLatin_c, chLatin_a, chLatin_t, chLatin_a, chLatin_l, chLatin_o, chLatin_g, chNull
255     };
256
257     // Parse the catalog with the internal parser pool.
258
259     if (log.isDebugEnabled()) {
260         auto_ptr_char temp(pathname);
261         log.debug("loading XML catalog from %s", temp.get());
262     }
263
264     LocalFileInputSource fsrc(NULL,pathname);
265     Wrapper4InputSource domsrc(&fsrc,false);
266     try {
267         DOMDocument* doc=XMLToolingConfig::getConfig().getParser().parse(domsrc);
268         XercesJanitor<DOMDocument> janitor(doc);
269
270         // Check root element.
271         const DOMElement* root=doc->getDocumentElement();
272         if (!XMLHelper::isNodeNamed(root,CATALOG_NS,catalog)) {
273             auto_ptr_char temp(pathname);
274             log.error("unknown root element, failed to load XML catalog from %s", temp.get());
275             return false;
276         }
277
278         // Fetch all the <system> elements.
279         DOMNodeList* mappings=root->getElementsByTagNameNS(CATALOG_NS,system);
280         Lock lock(m_lock);
281         for (XMLSize_t i=0; i<mappings->getLength(); i++) {
282             root=static_cast<DOMElement*>(mappings->item(i));
283             const XMLCh* from=root->getAttributeNS(NULL,systemId);
284             const XMLCh* to=root->getAttributeNS(NULL,uri);
285             m_schemaLocMap[from]=to;
286         }
287         m_schemaLocations.erase();
288         for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
289     }
290     catch (exception& e) {
291         log.error("catalog loader caught exception: %s", e.what());
292         return false;
293     }
294
295     return true;
296 }
297
298 #ifdef XMLTOOLING_XERCESC_COMPLIANT_DOMLS
299 DOMLSInput* ParserPool::resolveResource(
300             const XMLCh *const resourceType,
301             const XMLCh *const namespaceUri,
302             const XMLCh *const publicId,
303             const XMLCh *const systemId,
304             const XMLCh *const baseURI
305             )
306 #else
307 DOMInputSource* ParserPool::resolveEntity(
308     const XMLCh* const publicId, const XMLCh* const systemId, const XMLCh* const baseURI
309     )
310 #endif
311 {
312 #if _DEBUG
313     xmltooling::NDC ndc("resolveEntity");
314 #endif
315     if (!systemId)
316         return NULL;
317
318     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
319     if (log.isDebugEnabled()) {
320         auto_ptr_char sysId(systemId);
321         auto_ptr_char base(baseURI);
322         log.debug("asked to resolve %s with baseURI %s",sysId.get(),base.get() ? base.get() : "(null)");
323     }
324
325     // Find well-known schemas in the specified location.
326     map<xstring,xstring>::const_iterator i=m_schemaLocMap.find(systemId);
327     if (i!=m_schemaLocMap.end())
328         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
329
330     // Check for entity as a value in the map.
331     for (i=m_schemaLocMap.begin(); i!=m_schemaLocMap.end(); ++i) {
332         if (XMLString::endsWith(i->second.c_str(), systemId))
333             return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
334     }
335
336     // We'll allow anything without embedded slashes.
337     if (XMLString::indexOf(systemId, chForwardSlash)==-1)
338         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,systemId));
339
340     // Shortcircuit the request.
341     auto_ptr_char temp(systemId);
342     log.debug("unauthorized entity request (%s), blocking it", temp.get());
343     static const XMLByte nullbuf[] = {0};
344     return new Wrapper4InputSource(new MemBufInputSource(nullbuf,0,systemId));
345 }
346
347 #ifdef XMLTOOLING_XERCESC_COMPLIANT_DOMLS
348
349 DOMLSParser* ParserPool::createBuilder()
350 {
351     static const XMLCh impltype[] = { chLatin_L, chLatin_S, chNull };
352     DOMImplementation* impl=DOMImplementationRegistry::getDOMImplementation(impltype);
353     DOMLSParser* parser=static_cast<DOMImplementationLS*>(impl)->createLSParser(DOMImplementationLS::MODE_SYNCHRONOUS,NULL);
354     parser->getDomConfig()->setParameter(XMLUni::fgDOMNamespaces, m_namespaceAware);
355     if (m_schemaAware) {
356         parser->getDomConfig()->setParameter(XMLUni::fgDOMNamespaces, true);
357         parser->getDomConfig()->setParameter(XMLUni::fgXercesSchema, true);
358         parser->getDomConfig()->setParameter(XMLUni::fgDOMValidate, true);
359         parser->getDomConfig()->setParameter(XMLUni::fgXercesCacheGrammarFromParse, true);
360
361         // We build a "fake" schema location hint that binds each namespace to itself.
362         // This ensures the entity resolver will be given the namespace as a systemId it can check.
363         parser->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(m_schemaLocations.c_str()));
364     }
365     parser->getDomConfig()->setParameter(XMLUni::fgXercesUserAdoptsDOMDocument, true);
366     parser->getDomConfig()->setParameter(XMLUni::fgXercesDisableDefaultEntityResolution, true);
367     parser->getDomConfig()->setParameter(XMLUni::fgDOMResourceResolver, dynamic_cast<DOMLSResourceResolver*>(this));
368     parser->getDomConfig()->setParameter(XMLUni::fgXercesSecurityManager, m_security);
369     return parser;
370 }
371
372 DOMLSParser* ParserPool::checkoutBuilder()
373 {
374     Lock lock(m_lock);
375     if (m_pool.empty()) {
376         DOMLSParser* builder=createBuilder();
377         return builder;
378     }
379     DOMLSParser* p=m_pool.top();
380     m_pool.pop();
381     if (m_schemaAware)
382         p->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(m_schemaLocations.c_str()));
383     return p;
384 }
385
386 void ParserPool::checkinBuilder(DOMLSParser* builder)
387 {
388     if (builder) {
389         Lock lock(m_lock);
390         m_pool.push(builder);
391     }
392 }
393
394 #else
395
396 DOMBuilder* ParserPool::createBuilder()
397 {
398     static const XMLCh impltype[] = { chLatin_L, chLatin_S, chNull };
399     DOMImplementation* impl=DOMImplementationRegistry::getDOMImplementation(impltype);
400     DOMBuilder* parser=static_cast<DOMImplementationLS*>(impl)->createDOMBuilder(DOMImplementationLS::MODE_SYNCHRONOUS,0);
401     parser->setFeature(XMLUni::fgDOMNamespaces, m_namespaceAware);
402     if (m_schemaAware) {
403         parser->setFeature(XMLUni::fgDOMNamespaces, true);
404         parser->setFeature(XMLUni::fgXercesSchema, true);
405         parser->setFeature(XMLUni::fgDOMValidation, true);
406         parser->setFeature(XMLUni::fgXercesCacheGrammarFromParse, true);
407
408         // We build a "fake" schema location hint that binds each namespace to itself.
409         // This ensures the entity resolver will be given the namespace as a systemId it can check.
410         parser->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
411     }
412     parser->setProperty(XMLUni::fgXercesSecurityManager, m_security);
413     parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument, true);
414     parser->setFeature(XMLUni::fgXercesDisableDefaultEntityResolution, true);
415     parser->setEntityResolver(this);
416     return parser;
417 }
418
419 DOMBuilder* ParserPool::checkoutBuilder()
420 {
421     Lock lock(m_lock);
422     if (m_pool.empty()) {
423         DOMBuilder* builder=createBuilder();
424         return builder;
425     }
426     DOMBuilder* p=m_pool.top();
427     m_pool.pop();
428     if (m_schemaAware)
429         p->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
430     return p;
431 }
432
433 void ParserPool::checkinBuilder(DOMBuilder* builder)
434 {
435     if (builder) {
436         Lock lock(m_lock);
437         m_pool.push(builder);
438     }
439 }
440
441 #endif
442
443 xsecsize_t StreamInputSource::StreamBinInputStream::readBytes(XMLByte* const toFill, const xsecsize_t maxToRead)
444 {
445     XMLByte* target=toFill;
446     xsecsize_t bytes_read=0,request=maxToRead;
447
448     // Fulfill the rest by reading from the stream.
449     if (request && !m_is.eof() && !m_is.fail()) {
450         try {
451             m_is.read(reinterpret_cast<char* const>(target),request);
452             m_pos+=m_is.gcount();
453             bytes_read+=m_is.gcount();
454         }
455         catch(ios_base::failure& e) {
456             Category::getInstance(XMLTOOLING_LOGCAT".StreamInputSource").critStream()
457                 << "XML::StreamInputSource::StreamBinInputStream::readBytes caught an exception: " << e.what()
458                 << logging::eol;
459             *toFill=0;
460             return 0;
461         }
462     }
463     return bytes_read;
464 }
465
466 #ifdef XMLTOOLING_LITE
467
468 URLInputSource::URLInputSource(const XMLCh* url, const char* systemId) : InputSource(systemId), m_url(url)
469 {
470 }
471
472 URLInputSource::URLInputSource(const DOMElement* e, const char* systemId) : InputSource(systemId)
473 {
474     static const XMLCh uri[] = UNICODE_LITERAL_3(u,r,i);
475     static const XMLCh url[] = UNICODE_LITERAL_3(u,r,l);
476
477     const XMLCh* attr = e->getAttributeNS(NULL, url);
478     if (!attr || !*attr) {
479         attr = e->getAttributeNS(NULL, uri);
480         if (!attr || !*attr)
481             throw IOException("No URL supplied via DOM to URLInputSource constructor.");
482     }
483
484     m_url.setURL(attr);
485 }
486
487 BinInputStream* URLInputSource::makeStream() const
488 {
489     // Ask the URL to create us an appropriate input stream
490     return m_url.makeNewStream();
491 }
492
493 #else
494
495 URLInputSource::URLInputSource(const XMLCh* url, const char* systemId)
496     : InputSource(systemId), m_url(url), m_root(NULL)
497 {
498 }
499
500 URLInputSource::URLInputSource(const DOMElement* e, const char* systemId)
501     : InputSource(systemId), m_root(e)
502 {
503 }
504
505 BinInputStream* URLInputSource::makeStream() const
506 {
507     return m_root ? new CurlURLInputStream(m_root) : new CurlURLInputStream(m_url.get());
508 }
509
510 #endif