Rework support for libcurl-based input to parser.
[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 #ifdef HAVE_GOOD_STL
228     m_schemaLocMap[nsURI]=pathname;
229     m_schemaLocations.erase();
230     for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
231 #else
232     auto_ptr_char n(nsURI);
233     m_schemaLocMap[n.get()]=p.get();
234     m_schemaLocations.erase();
235     for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<string>(m_schemaLocations,' '));
236 #endif
237
238     return true;
239 }
240
241 bool ParserPool::loadCatalog(const XMLCh* pathname)
242 {
243 #if _DEBUG
244     xmltooling::NDC ndc("loadCatalog");
245 #endif
246     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
247
248     // XML constants
249     static const XMLCh catalog[] =  UNICODE_LITERAL_7(c,a,t,a,l,o,g);
250     static const XMLCh system[] =   UNICODE_LITERAL_6(s,y,s,t,e,m);
251     static const XMLCh systemId[] = UNICODE_LITERAL_8(s,y,s,t,e,m,I,d);
252     static const XMLCh uri[] =      UNICODE_LITERAL_3(u,r,i);
253     static const XMLCh CATALOG_NS[] = {
254         chLatin_u, chLatin_r, chLatin_n, chColon,
255         chLatin_o, chLatin_a, chLatin_s, chLatin_i, chLatin_s, chColon,
256         chLatin_n, chLatin_a, chLatin_m, chLatin_e, chLatin_s, chColon,
257         chLatin_t, chLatin_c, chColon,
258         chLatin_e, chLatin_n, chLatin_t, chLatin_i, chLatin_t, chLatin_y, chColon,
259         chLatin_x, chLatin_m, chLatin_l, chLatin_n, chLatin_s, chColon,
260         chLatin_x, chLatin_m, chLatin_l, chColon,
261         chLatin_c, chLatin_a, chLatin_t, chLatin_a, chLatin_l, chLatin_o, chLatin_g, chNull
262     };
263
264     // Parse the catalog with the internal parser pool.
265
266     if (log.isDebugEnabled()) {
267         auto_ptr_char temp(pathname);
268         log.debug("loading XML catalog from %s", temp.get());
269     }
270
271     LocalFileInputSource fsrc(NULL,pathname);
272     Wrapper4InputSource domsrc(&fsrc,false);
273     try {
274         DOMDocument* doc=XMLToolingConfig::getConfig().getParser().parse(domsrc);
275         XercesJanitor<DOMDocument> janitor(doc);
276         
277         // Check root element.
278         const DOMElement* root=doc->getDocumentElement();
279         if (!XMLHelper::isNodeNamed(root,CATALOG_NS,catalog)) {
280             auto_ptr_char temp(pathname);
281             log.error("unknown root element, failed to load XML catalog from %s", temp.get());
282             return false;
283         }
284         
285         // Fetch all the <system> elements.
286         DOMNodeList* mappings=root->getElementsByTagNameNS(CATALOG_NS,system);
287         Lock lock(m_lock);
288         for (XMLSize_t i=0; i<mappings->getLength(); i++) {
289             root=static_cast<DOMElement*>(mappings->item(i));
290             const XMLCh* from=root->getAttributeNS(NULL,systemId);
291             const XMLCh* to=root->getAttributeNS(NULL,uri);
292 #ifdef HAVE_GOOD_STL
293             m_schemaLocMap[from]=to;
294 #else
295             auto_ptr_char f(from);
296             auto_ptr_char t(to);
297             m_schemaLocMap[f.get()]=t.get();
298 #endif
299         }
300         m_schemaLocations.erase();
301 #ifdef HAVE_GOOD_STL
302         for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
303 #else
304         for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<string>(m_schemaLocations,' '));
305 #endif
306     }
307     catch (exception& e) {
308         log.error("catalog loader caught exception: %s", e.what());
309         return false;
310     }
311
312     return true;
313 }
314
315 #ifdef XMLTOOLING_XERCESC_COMPLIANT_DOMLS
316 DOMLSInput* ParserPool::resolveResource(
317             const XMLCh *const resourceType,
318             const XMLCh *const namespaceUri,
319             const XMLCh *const publicId,
320             const XMLCh *const systemId,
321             const XMLCh *const baseURI
322             )
323 #else
324 DOMInputSource* ParserPool::resolveEntity(
325     const XMLCh* const publicId, const XMLCh* const systemId, const XMLCh* const baseURI
326     )
327 #endif
328 {
329 #if _DEBUG
330     xmltooling::NDC ndc("resolveEntity");
331 #endif
332     if (!systemId)
333         return NULL;
334
335     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
336     if (log.isDebugEnabled()) {
337         auto_ptr_char sysId(systemId);
338         auto_ptr_char base(baseURI);
339         log.debug("asked to resolve %s with baseURI %s",sysId.get(),base.get() ? base.get() : "(null)");
340     }
341
342 #ifdef HAVE_GOOD_STL
343     // Find well-known schemas in the specified location.
344     map<xstring,xstring>::const_iterator i=m_schemaLocMap.find(systemId);
345     if (i!=m_schemaLocMap.end())
346         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
347
348     // Check for entity as a value in the map.
349     for (i=m_schemaLocMap.begin(); i!=m_schemaLocMap.end(); ++i) {
350         if (XMLString::endsWith(i->second.c_str(), systemId))
351             return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
352     }
353
354     // We'll allow anything without embedded slashes.
355     if (XMLString::indexOf(systemId, chForwardSlash)==-1)
356         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,systemId));
357 #else
358     // Find well-known schemas in the specified location.
359     auto_ptr_char temp(systemId);
360     map<string,string>::const_iterator i=m_schemaLocMap.find(temp.get());
361     if (i!=m_schemaLocMap.end()) {
362         auto_ptr_XMLCh temp2(i->second.c_str());
363         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,temp2.get()));
364     }
365
366     // Check for entity as a value in the map.
367     for (i=m_schemaLocMap.begin(); i!=m_schemaLocMap.end(); ++i) {
368         auto_ptr_XMLCh temp2(i->second.c_str());
369         if (XMLString::endsWith(temp2.get(), systemId))
370             return new Wrapper4InputSource(new LocalFileInputSource(baseURI,temp2.get()));
371     }
372
373     // We'll allow anything without embedded slashes.
374     if (XMLString::indexOf(systemId, chForwardSlash)==-1)
375         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,systemId));
376 #endif    
377
378     // Shortcircuit the request.
379 #ifdef HAVE_GOOD_STL
380     auto_ptr_char temp(systemId);
381 #endif
382     log.debug("unauthorized entity request (%s), blocking it", temp.get());
383     static const XMLByte nullbuf[] = {0};
384     return new Wrapper4InputSource(new MemBufInputSource(nullbuf,0,systemId));
385 }
386
387 #ifdef XMLTOOLING_XERCESC_COMPLIANT_DOMLS
388
389 DOMLSParser* ParserPool::createBuilder()
390 {
391     static const XMLCh impltype[] = { chLatin_L, chLatin_S, chNull };
392     DOMImplementation* impl=DOMImplementationRegistry::getDOMImplementation(impltype);
393     DOMLSParser* parser=static_cast<DOMImplementationLS*>(impl)->createLSParser(DOMImplementationLS::MODE_SYNCHRONOUS,NULL);
394     parser->getDomConfig()->setParameter(XMLUni::fgDOMNamespaces, m_namespaceAware);
395     if (m_schemaAware) {
396         parser->getDomConfig()->setParameter(XMLUni::fgDOMNamespaces, true);
397         parser->getDomConfig()->setParameter(XMLUni::fgXercesSchema, true);
398         parser->getDomConfig()->setParameter(XMLUni::fgDOMValidate, true);
399         parser->getDomConfig()->setParameter(XMLUni::fgXercesCacheGrammarFromParse, true);
400         
401         // We build a "fake" schema location hint that binds each namespace to itself.
402         // This ensures the entity resolver will be given the namespace as a systemId it can check. 
403 #ifdef HAVE_GOOD_STL
404         parser->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(m_schemaLocations.c_str()));
405 #else
406         auto_ptr_XMLCh temp(m_schemaLocations.c_str());
407         parser->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(temp.get()));
408 #endif
409     }
410     parser->getDomConfig()->setParameter(XMLUni::fgXercesUserAdoptsDOMDocument, true);
411     parser->getDomConfig()->setParameter(XMLUni::fgXercesDisableDefaultEntityResolution, true);
412     parser->getDomConfig()->setParameter(XMLUni::fgDOMResourceResolver, dynamic_cast<DOMLSResourceResolver*>(this));
413     parser->getDomConfig()->setParameter(XMLUni::fgXercesSecurityManager, m_security);
414     return parser;
415 }
416
417 DOMLSParser* ParserPool::checkoutBuilder()
418 {
419     Lock lock(m_lock);
420     if (m_pool.empty()) {
421         DOMLSParser* builder=createBuilder();
422         return builder;
423     }
424     DOMLSParser* p=m_pool.top();
425     m_pool.pop();
426     if (m_schemaAware) {
427 #ifdef HAVE_GOOD_STL
428         p->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(m_schemaLocations.c_str()));
429 #else
430         auto_ptr_XMLCh temp2(m_schemaLocations.c_str());
431         p->getDomConfig()->setParameter(XMLUni::fgXercesSchemaExternalSchemaLocation, const_cast<XMLCh*>(temp2.get()));
432 #endif
433     }
434     return p;
435 }
436
437 void ParserPool::checkinBuilder(DOMLSParser* builder)
438 {
439     if (builder) {
440         Lock lock(m_lock);
441         m_pool.push(builder);
442     }
443 }
444
445 #else
446
447 DOMBuilder* ParserPool::createBuilder()
448 {
449     static const XMLCh impltype[] = { chLatin_L, chLatin_S, chNull };
450     DOMImplementation* impl=DOMImplementationRegistry::getDOMImplementation(impltype);
451     DOMBuilder* parser=static_cast<DOMImplementationLS*>(impl)->createDOMBuilder(DOMImplementationLS::MODE_SYNCHRONOUS,0);
452     parser->setFeature(XMLUni::fgDOMNamespaces, m_namespaceAware);
453     if (m_schemaAware) {
454         parser->setFeature(XMLUni::fgDOMNamespaces, true);
455         parser->setFeature(XMLUni::fgXercesSchema, true);
456         parser->setFeature(XMLUni::fgDOMValidation, true);
457         parser->setFeature(XMLUni::fgXercesCacheGrammarFromParse, true);
458         
459         // We build a "fake" schema location hint that binds each namespace to itself.
460         // This ensures the entity resolver will be given the namespace as a systemId it can check. 
461 #ifdef HAVE_GOOD_STL
462         parser->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
463 #else
464         auto_ptr_XMLCh temp(m_schemaLocations.c_str());
465         parser->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(temp.get()));
466 #endif
467     }
468     parser->setProperty(XMLUni::fgXercesSecurityManager, m_security);
469     parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument, true);
470     parser->setFeature(XMLUni::fgXercesDisableDefaultEntityResolution, true);
471     parser->setEntityResolver(this);
472     return parser;
473 }
474
475 DOMBuilder* ParserPool::checkoutBuilder()
476 {
477     Lock lock(m_lock);
478     if (m_pool.empty()) {
479         DOMBuilder* builder=createBuilder();
480         return builder;
481     }
482     DOMBuilder* p=m_pool.top();
483     m_pool.pop();
484     if (m_schemaAware) {
485 #ifdef HAVE_GOOD_STL
486         p->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
487 #else
488         auto_ptr_XMLCh temp2(m_schemaLocations.c_str());
489         p->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(temp2.get()));
490 #endif
491     }
492     return p;
493 }
494
495 void ParserPool::checkinBuilder(DOMBuilder* builder)
496 {
497     if (builder) {
498         Lock lock(m_lock);
499         m_pool.push(builder);
500     }
501 }
502
503 #endif
504
505 xsecsize_t StreamInputSource::StreamBinInputStream::readBytes(XMLByte* const toFill, const xsecsize_t maxToRead)
506 {
507     XMLByte* target=toFill;
508     xsecsize_t bytes_read=0,request=maxToRead;
509
510     // Fulfill the rest by reading from the stream.
511     if (request && !m_is.eof() && !m_is.fail()) {
512         try {
513             m_is.read(reinterpret_cast<char* const>(target),request);
514             m_pos+=m_is.gcount();
515             bytes_read+=m_is.gcount();
516         }
517         catch(ios_base::failure& e) {
518             Category::getInstance(XMLTOOLING_LOGCAT".StreamInputSource").critStream()
519                 << "XML::StreamInputSource::StreamBinInputStream::readBytes caught an exception: " << e.what()
520                 << logging::eol;
521             *toFill=0;
522             return 0;
523         }
524     }
525     return bytes_read;
526 }
527
528 #ifdef XMLTOOLING_LITE
529
530 URLInputSource::URLInputSource(const XMLCh* url, const char* systemId) : InputSource(systemId), m_url(url)
531 {
532 }
533
534 URLInputSource::URLInputSource(const DOMElement* e, const char* systemId) : InputSource(systemId)
535 {
536     static const XMLCh uri[] = UNICODE_LITERAL_3(u,r,i);
537     static const XMLCh url[] = UNICODE_LITERAL_3(u,r,l);
538
539     const XMLCh* attr = e->getAttributeNS(NULL, url);
540     if (!attr || !*attr) {
541         attr = e->getAttributeNS(NULL, uri);
542         if (!attr || !*attr)
543             throw IOException("No URL supplied via DOM to URLInputSource constructor.");
544     }
545
546     m_url.setURL(attr);
547 }
548
549 BinInputStream* URLInputSource::makeStream() const
550 {
551     // Ask the URL to create us an appropriate input stream
552     return m_url.makeNewStream();
553 }
554
555 #else
556
557 URLInputSource::URLInputSource(const XMLCh* url, const char* systemId)
558     : InputSource(systemId), m_url(url), m_root(NULL)
559 {
560 }
561
562 URLInputSource::URLInputSource(const DOMElement* e, const char* systemId)
563     : InputSource(systemId), m_root(e)
564 {
565 }
566
567 BinInputStream* URLInputSource::makeStream() const
568 {
569     return m_root ? new CurlURLInputStream(m_root) : new CurlURLInputStream(m_url.get());
570 }
571
572 #endif