Install SecurityManager to block entity expansion.
[shibboleth/xmltooling.git] / xmltooling / util / ParserPool.cpp
1 /*
2  *  Copyright 2001-2007 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  * XML parsing
21  */
22
23 #include "internal.h"
24 #include "exceptions.h"
25 #include "logging.h"
26 #include "util/NDC.h"
27 #include "util/ParserPool.h"
28 #include "util/XMLHelper.h"
29
30 #include <algorithm>
31 #include <functional>
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <xercesc/util/PlatformUtils.hpp>
35 #include <xercesc/util/XMLUniDefs.hpp>
36 #include <xercesc/sax/SAXException.hpp>
37 #include <xercesc/framework/MemBufInputSource.hpp>
38 #include <xercesc/framework/LocalFileInputSource.hpp>
39 #include <xercesc/framework/Wrapper4InputSource.hpp>
40
41 using namespace xmltooling::logging;
42 using namespace xmltooling;
43 using namespace std;
44
45 ParserPool::ParserPool(bool namespaceAware, bool schemaAware)
46     : m_namespaceAware(namespaceAware), m_schemaAware(schemaAware), m_lock(Mutex::create()), m_security(new SecurityManager()) {}
47
48 ParserPool::~ParserPool()
49 {
50     while(!m_pool.empty()) {
51         m_pool.top()->release();
52         m_pool.pop();
53     }
54     delete m_lock;
55     delete m_security;
56 }
57
58 DOMDocument* ParserPool::newDocument()
59 {
60     return DOMImplementationRegistry::getDOMImplementation(NULL)->createDocument();
61 }
62
63 DOMDocument* ParserPool::parse(DOMInputSource& domsrc)
64 {
65     DOMBuilder* parser=checkoutBuilder();
66     XercesJanitor<DOMBuilder> janitor(parser);
67     try {
68         DOMDocument* doc=parser->parse(domsrc);
69         parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument,true);
70         checkinBuilder(janitor.release());
71         return doc;
72     }
73     catch (XMLException&) {
74         checkinBuilder(janitor.release());
75         throw;
76     }
77     catch (XMLToolingException&) {
78         checkinBuilder(janitor.release());
79         throw;
80     }
81 }
82
83 DOMDocument* ParserPool::parse(istream& is)
84 {
85     StreamInputSource src(is);
86     Wrapper4InputSource domsrc(&src,false);
87     return parse(domsrc);
88 }
89
90 // Functor to double its argument separated by a character and append to a buffer
91 template <class T> class doubleit
92 {
93 public:
94     doubleit(T& t, const typename T::value_type& s) : temp(t), sep(s) {}
95     void operator() (const pair<const T,T>& s) { temp += s.first + sep + s.first + sep; }
96     T& temp;
97     const typename T::value_type& sep;
98 };
99
100 bool ParserPool::loadSchema(const XMLCh* nsURI, const XMLCh* pathname)
101 {
102     // Just check the pathname and then directly register the pair into the map.
103     
104     auto_ptr_char p(pathname);
105 #ifdef WIN32
106     struct _stat stat_buf;
107     if (_stat(p.get(), &stat_buf) != 0)
108 #else
109     struct stat stat_buf;
110     if (stat(p.get(), &stat_buf) != 0)
111 #endif
112     {
113 #if _DEBUG
114         xmltooling::NDC ndc("loadSchema");
115 #endif
116         Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
117         auto_ptr_char n(nsURI);
118         log.error("failed to load schema for (%s), file not found (%s)",n.get(),p.get());
119         return false;
120     }
121
122     Lock lock(m_lock);
123 #ifdef HAVE_GOOD_STL
124     m_schemaLocMap[nsURI]=pathname;
125     m_schemaLocations.erase();
126     for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
127 #else
128     auto_ptr_char n(nsURI);
129     m_schemaLocMap[n.get()]=p.get();
130     m_schemaLocations.erase();
131     for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<string>(m_schemaLocations,' '));
132 #endif
133
134     return true;
135 }
136
137 bool ParserPool::loadCatalog(const XMLCh* pathname)
138 {
139 #if _DEBUG
140     xmltooling::NDC ndc("loadCatalog");
141 #endif
142     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
143
144     // XML constants
145     static const XMLCh catalog[] =  UNICODE_LITERAL_7(c,a,t,a,l,o,g);
146     static const XMLCh system[] =   UNICODE_LITERAL_6(s,y,s,t,e,m);
147     static const XMLCh systemId[] = UNICODE_LITERAL_8(s,y,s,t,e,m,I,d);
148     static const XMLCh uri[] =      UNICODE_LITERAL_3(u,r,i);
149     static const XMLCh CATALOG_NS[] = {
150         chLatin_u, chLatin_r, chLatin_n, chColon,
151         chLatin_o, chLatin_a, chLatin_s, chLatin_i, chLatin_s, chColon,
152         chLatin_n, chLatin_a, chLatin_m, chLatin_e, chLatin_s, chColon,
153         chLatin_t, chLatin_c, chColon,
154         chLatin_e, chLatin_n, chLatin_t, chLatin_i, chLatin_t, chLatin_y, chColon,
155         chLatin_x, chLatin_m, chLatin_l, chLatin_n, chLatin_s, chColon,
156         chLatin_x, chLatin_m, chLatin_l, chColon,
157         chLatin_c, chLatin_a, chLatin_t, chLatin_a, chLatin_l, chLatin_o, chLatin_g, chNull
158     };
159
160     // Parse the catalog with the internal parser pool.
161
162     if (log.isDebugEnabled()) {
163         auto_ptr_char temp(pathname);
164         log.debug("loading XML catalog from %s", temp.get());
165     }
166
167     LocalFileInputSource fsrc(NULL,pathname);
168     Wrapper4InputSource domsrc(&fsrc,false);
169     try {
170         DOMDocument* doc=XMLToolingConfig::getConfig().getParser().parse(domsrc);
171         XercesJanitor<DOMDocument> janitor(doc);
172         
173         // Check root element.
174         const DOMElement* root=doc->getDocumentElement();
175         if (!XMLHelper::isNodeNamed(root,CATALOG_NS,catalog)) {
176             auto_ptr_char temp(pathname);
177             log.error("unknown root element, failed to load XML catalog from %s", temp.get());
178             return false;
179         }
180         
181         // Fetch all the <system> elements.
182         DOMNodeList* mappings=root->getElementsByTagNameNS(CATALOG_NS,system);
183         Lock lock(m_lock);
184         for (XMLSize_t i=0; i<mappings->getLength(); i++) {
185             root=static_cast<DOMElement*>(mappings->item(i));
186             const XMLCh* from=root->getAttributeNS(NULL,systemId);
187             const XMLCh* to=root->getAttributeNS(NULL,uri);
188 #ifdef HAVE_GOOD_STL
189             m_schemaLocMap[from]=to;
190 #else
191             auto_ptr_char f(from);
192             auto_ptr_char t(to);
193             m_schemaLocMap[f.get()]=t.get();
194 #endif
195         }
196         m_schemaLocations.erase();
197 #ifdef HAVE_GOOD_STL
198         for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<xstring>(m_schemaLocations,chSpace));
199 #else
200         for_each(m_schemaLocMap.begin(),m_schemaLocMap.end(),doubleit<string>(m_schemaLocations,' '));
201 #endif
202     }
203     catch (exception& e) {
204         log.error("catalog loader caught exception: %s", e.what());
205         return false;
206     }
207
208     return true;
209 }
210
211 DOMInputSource* ParserPool::resolveEntity(const XMLCh* const publicId, const XMLCh* const systemId, const XMLCh* const baseURI)
212 {
213 #if _DEBUG
214     xmltooling::NDC ndc("resolveEntity");
215 #endif
216     if (!systemId)
217         return NULL;
218
219     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
220     if (log.isDebugEnabled()) {
221         auto_ptr_char sysId(systemId);
222         auto_ptr_char base(baseURI);
223         log.debug("asked to resolve %s with baseURI %s",sysId.get(),base.get() ? base.get() : "(null)");
224     }
225
226 #ifdef HAVE_GOOD_STL
227     // Find well-known schemas in the specified location.
228     map<xstring,xstring>::const_iterator i=m_schemaLocMap.find(systemId);
229     if (i!=m_schemaLocMap.end())
230         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
231
232     // Check for entity as a value in the map.
233     for (i=m_schemaLocMap.begin(); i!=m_schemaLocMap.end(); ++i) {
234         if (XMLString::endsWith(i->second.c_str(), systemId))
235             return new Wrapper4InputSource(new LocalFileInputSource(baseURI,i->second.c_str()));
236     }
237
238     // We'll allow anything without embedded slashes.
239     if (XMLString::indexOf(systemId, chForwardSlash)==-1)
240         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,systemId));
241 #else
242     // Find well-known schemas in the specified location.
243     auto_ptr_char temp(systemId);
244     map<string,string>::const_iterator i=m_schemaLocMap.find(temp.get());
245     if (i!=m_schemaLocMap.end()) {
246         auto_ptr_XMLCh temp2(i->second.c_str());
247         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,temp2.get()));
248     }
249
250     // Check for entity as a value in the map.
251     for (i=m_schemaLocMap.begin(); i!=m_schemaLocMap.end(); ++i) {
252         auto_ptr_XMLCh temp2(i->second.c_str());
253         if (XMLString::endsWith(temp2.get(), systemId))
254             return new Wrapper4InputSource(new LocalFileInputSource(baseURI,temp2.get()));
255     }
256
257     // We'll allow anything without embedded slashes.
258     if (XMLString::indexOf(systemId, chForwardSlash)==-1)
259         return new Wrapper4InputSource(new LocalFileInputSource(baseURI,systemId));
260 #endif    
261
262     // Shortcircuit the request.
263 #ifdef HAVE_GOOD_STL
264     auto_ptr_char temp(systemId);
265 #endif
266     log.debug("unauthorized entity request (%s), blocking it", temp.get());
267     static const XMLByte nullbuf[] = {0};
268     return new Wrapper4InputSource(new MemBufInputSource(nullbuf,0,systemId));
269 }
270
271 bool ParserPool::handleError(const DOMError& e)
272 {
273 #ifdef _DEBUG
274     xmltooling::NDC ndc("handleError");
275 #endif
276     Category& log=Category::getInstance(XMLTOOLING_LOGCAT".ParserPool");
277     DOMLocator* locator=e.getLocation();
278     auto_ptr_char temp(e.getMessage());
279
280     switch (e.getSeverity()) {
281         case DOMError::DOM_SEVERITY_WARNING:
282             log.warnStream() << "warning on line " << locator->getLineNumber()
283                 << ", column " << locator->getColumnNumber()
284                 << ", message: " << temp.get() << logging::eol;
285             return true;
286
287         case DOMError::DOM_SEVERITY_ERROR:
288             log.errorStream() << "error on line " << locator->getLineNumber()
289                 << ", column " << locator->getColumnNumber()
290                 << ", message: " << temp.get() << logging::eol;
291             throw XMLParserException(string("error during XML parsing: ") + (temp.get() ? temp.get() : "no message"));
292
293         case DOMError::DOM_SEVERITY_FATAL_ERROR:
294             log.critStream() << "fatal error on line " << locator->getLineNumber()
295                 << ", column " << locator->getColumnNumber()
296                 << ", message: " << temp.get() << logging::eol;
297             throw XMLParserException(string("fatal error during XML parsing: ") + (temp.get() ? temp.get() : "no message"));
298     }
299     throw XMLParserException(string("unclassified error during XML parsing: ") + (temp.get() ? temp.get() : "no message"));
300 }
301
302 DOMBuilder* ParserPool::createBuilder()
303 {
304     static const XMLCh impltype[] = { chLatin_L, chLatin_S, chNull };
305     DOMImplementation* impl=DOMImplementationRegistry::getDOMImplementation(impltype);
306     DOMBuilder* parser=static_cast<DOMImplementationLS*>(impl)->createDOMBuilder(DOMImplementationLS::MODE_SYNCHRONOUS,0);
307     if (m_namespaceAware)
308         parser->setFeature(XMLUni::fgDOMNamespaces,true);
309     if (m_schemaAware) {
310         parser->setFeature(XMLUni::fgXercesSchema,true);
311         parser->setFeature(XMLUni::fgDOMValidation,true);
312         parser->setFeature(XMLUni::fgXercesCacheGrammarFromParse,true);
313         parser->setFeature(XMLUni::fgXercesValidationErrorAsFatal,true);
314         
315         // We build a "fake" schema location hint that binds each namespace to itself.
316         // This ensures the entity resolver will be given the namespace as a systemId it can check. 
317 #ifdef HAVE_GOOD_STL
318         parser->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
319 #else
320         auto_ptr_XMLCh temp(m_schemaLocations.c_str());
321         parser->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(temp.get()));
322 #endif
323     }
324     parser->setProperty(XMLUni::fgXercesSecurityManager, m_security);
325     parser->setFeature(XMLUni::fgXercesUserAdoptsDOMDocument,true);
326     parser->setEntityResolver(this);
327     parser->setErrorHandler(this);
328     return parser;
329 }
330
331 DOMBuilder* ParserPool::checkoutBuilder()
332 {
333     Lock lock(m_lock);
334     if (m_pool.empty()) {
335         DOMBuilder* builder=createBuilder();
336         return builder;
337     }
338     DOMBuilder* p=m_pool.top();
339     m_pool.pop();
340     if (m_schemaAware) {
341 #ifdef HAVE_GOOD_STL
342         p->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(m_schemaLocations.c_str()));
343 #else
344         auto_ptr_XMLCh temp2(m_schemaLocations.c_str());
345         p->setProperty(XMLUni::fgXercesSchemaExternalSchemaLocation,const_cast<XMLCh*>(temp2.get()));
346 #endif
347     }
348     return p;
349 }
350
351 void ParserPool::checkinBuilder(DOMBuilder* builder)
352 {
353     if (builder) {
354         Lock lock(m_lock);
355         m_pool.push(builder);
356     }
357 }
358
359 unsigned int StreamInputSource::StreamBinInputStream::readBytes(XMLByte* const toFill, const unsigned int maxToRead)
360 {
361     XMLByte* target=toFill;
362     unsigned int bytes_read=0,request=maxToRead;
363
364     // Fulfill the rest by reading from the stream.
365     if (request && !m_is.eof() && !m_is.fail()) {
366         try {
367             m_is.read(reinterpret_cast<char* const>(target),request);
368             m_pos+=m_is.gcount();
369             bytes_read+=m_is.gcount();
370         }
371         catch(ios_base::failure& e) {
372             Category::getInstance(XMLTOOLING_LOGCAT".StreamInputSource").critStream()
373                 << "XML::StreamInputSource::StreamBinInputStream::readBytes caught an exception: " << e.what()
374                 << logging::eol;
375             *toFill=0;
376             return 0;
377         }
378     }
379     return bytes_read;
380 }