Next refactoring stage, STPriv class gone, most of ST API gutted.
[shibboleth/cpp-sp.git] / shibsp / impl / XMLRequestMapper.cpp
1 /*\r
2  *  Copyright 2001-2005 Internet2\r
3  * \r
4  * Licensed under the Apache License, Version 2.0 (the "License");\r
5  * you may not use this file except in compliance with the License.\r
6  * You may obtain a copy of the License at\r
7  *\r
8  *     http://www.apache.org/licenses/LICENSE-2.0\r
9  *\r
10  * Unless required by applicable law or agreed to in writing, software\r
11  * distributed under the License is distributed on an "AS IS" BASIS,\r
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
13  * See the License for the specific language governing permissions and\r
14  * limitations under the License.\r
15  */\r
16 \r
17 /** XMLRequestMapper.cpp\r
18  * \r
19  * XML-based RequestMapper implementation\r
20  */\r
21 \r
22 #include "internal.h"\r
23 #include "AccessControl.h"\r
24 #include "RequestMapper.h"\r
25 #include "SPRequest.h"\r
26 #include "util/DOMPropertySet.h"\r
27 #include "util/SPConstants.h"\r
28 \r
29 #include <xmltooling/util/NDC.h>\r
30 #include <xmltooling/util/ReloadableXMLFile.h>\r
31 #include <xmltooling/util/XMLHelper.h>\r
32 #include <xercesc/util/XMLUniDefs.hpp>\r
33 \r
34 using namespace shibsp;\r
35 using namespace xmltooling;\r
36 using namespace log4cpp;\r
37 using namespace std;\r
38 \r
39 namespace shibsp {\r
40 \r
41     // Blocks access when an ACL plugin fails to load. \r
42     class AccessControlDummy : public AccessControl\r
43     {\r
44     public:\r
45         Lockable* lock() {\r
46             return this;\r
47         }\r
48         \r
49         void unlock() {}\r
50     \r
51         bool authorized(const SPRequest& request, const Session* session) const {\r
52             return false;\r
53         }\r
54     };\r
55 \r
56     class Override : public DOMPropertySet, public DOMNodeFilter\r
57     {\r
58     public:\r
59         Override() : m_base(NULL), m_acl(NULL) {}\r
60         Override(const DOMElement* e, Category& log, const Override* base=NULL);\r
61         ~Override();\r
62 \r
63         // PropertySet\r
64         pair<bool,bool> getBool(const char* name, const char* ns=NULL) const;\r
65         pair<bool,const char*> getString(const char* name, const char* ns=NULL) const;\r
66         pair<bool,const XMLCh*> getXMLString(const char* name, const char* ns=NULL) const;\r
67         pair<bool,unsigned int> getUnsignedInt(const char* name, const char* ns=NULL) const;\r
68         pair<bool,int> getInt(const char* name, const char* ns=NULL) const;\r
69         const PropertySet* getPropertySet(const char* name, const char* ns="urn:mace:shibboleth:target:config:1.0") const;\r
70         \r
71         // Provides filter to exclude special config elements.\r
72         short acceptNode(const DOMNode* node) const;\r
73 \r
74         const Override* locate(const char* path) const;\r
75         AccessControl* getAC() const { return (m_acl ? m_acl : (m_base ? m_base->getAC() : NULL)); }\r
76         \r
77     protected:\r
78         void loadACL(const DOMElement* e, Category& log);\r
79         \r
80         map<string,Override*> m_map;\r
81     \r
82     private:\r
83         const Override* m_base;\r
84         AccessControl* m_acl;\r
85     };\r
86 \r
87     class XMLRequestMapperImpl : public Override\r
88     {\r
89     public:\r
90         XMLRequestMapperImpl(const DOMElement* e, Category& log);\r
91 \r
92         ~XMLRequestMapperImpl() {\r
93             if (m_document)\r
94                 m_document->release();\r
95         }\r
96 \r
97         void setDocument(DOMDocument* doc) {\r
98             m_document = doc;\r
99         }\r
100     \r
101         const Override* findOverride(const char* vhost, const char* path) const;\r
102 \r
103     private:    \r
104         map<string,Override*> m_extras;\r
105         DOMDocument* m_document;\r
106     };\r
107 \r
108 #if defined (_MSC_VER)\r
109     #pragma warning( push )\r
110     #pragma warning( disable : 4250 )\r
111 #endif\r
112 \r
113     class XMLRequestMapper : public RequestMapper, public ReloadableXMLFile\r
114     {\r
115     public:\r
116         XMLRequestMapper(const DOMElement* e)\r
117                 : ReloadableXMLFile(e), m_impl(NULL), m_log(Category::getInstance(SHIBSP_LOGCAT".RequestMapper")) {\r
118             load();\r
119         }\r
120 \r
121         ~XMLRequestMapper() {\r
122             delete m_impl;\r
123         }\r
124 \r
125         Settings getSettings(const SPRequest& request) const;\r
126 \r
127     protected:\r
128         pair<bool,DOMElement*> load();\r
129 \r
130     private:\r
131         XMLRequestMapperImpl* m_impl;\r
132         Category& m_log;\r
133     };\r
134 \r
135 #if defined (_MSC_VER)\r
136     #pragma warning( pop )\r
137 #endif\r
138 \r
139     RequestMapper* SHIBSP_DLLLOCAL XMLRequestMapperFactory(const DOMElement* const & e)\r
140     {\r
141         return new XMLRequestMapper(e);\r
142     }\r
143 \r
144     static const XMLCh _AccessControl[] =            UNICODE_LITERAL_13(A,c,c,e,s,s,C,o,n,t,r,o,l);\r
145     static const XMLCh AccessControlProvider[] =    UNICODE_LITERAL_21(A,c,c,e,s,s,C,o,n,t,r,o,l,P,r,o,v,i,d,e,r);\r
146     static const XMLCh htaccess[] =                 UNICODE_LITERAL_8(h,t,a,c,c,e,s,s);\r
147     static const XMLCh Host[] =                     UNICODE_LITERAL_4(H,o,s,t);\r
148     static const XMLCh Path[] =                     UNICODE_LITERAL_4(P,a,t,h);\r
149     static const XMLCh name[] =                     UNICODE_LITERAL_4(n,a,m,e);\r
150     static const XMLCh type[] =                     UNICODE_LITERAL_4(t,y,p,e);\r
151 }\r
152 \r
153 void SHIBSP_API shibsp::registerRequestMappers()\r
154 {\r
155     SPConfig& conf=SPConfig::getConfig();\r
156     conf.RequestMapperManager.registerFactory(XML_REQUEST_MAPPER, XMLRequestMapperFactory);\r
157     conf.RequestMapperManager.registerFactory(NATIVE_REQUEST_MAPPER, XMLRequestMapperFactory);\r
158 }\r
159 \r
160 short Override::acceptNode(const DOMNode* node) const\r
161 {\r
162     if (!XMLString::equals(node->getNamespaceURI(),shibspconstants::SHIB1SPCONFIG_NS))\r
163         return FILTER_ACCEPT;\r
164     const XMLCh* name=node->getLocalName();\r
165     if (XMLString::equals(name,Host) ||\r
166         XMLString::equals(name,Path) ||\r
167         XMLString::equals(name,_AccessControl) ||\r
168         XMLString::equals(name,htaccess) ||\r
169         XMLString::equals(name,AccessControlProvider))\r
170         return FILTER_REJECT;\r
171 \r
172     return FILTER_ACCEPT;\r
173 }\r
174 \r
175 void Override::loadACL(const DOMElement* e, Category& log)\r
176 {\r
177     try {\r
178         const DOMElement* acl=XMLHelper::getFirstChildElement(e,htaccess);\r
179         if (acl) {\r
180             log.info("building Apache htaccess AccessControl provider...");\r
181             m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(HT_ACCESS_CONTROL,acl);\r
182         }\r
183         else {\r
184             acl=XMLHelper::getFirstChildElement(e,_AccessControl);\r
185             if (acl) {\r
186                 log.info("building XML-based AccessControl provider...");\r
187                 m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(XML_ACCESS_CONTROL,acl);\r
188             }\r
189             else {\r
190                 acl=XMLHelper::getFirstChildElement(e,AccessControlProvider);\r
191                 if (acl) {\r
192                     xmltooling::auto_ptr_char type(acl->getAttributeNS(NULL,type));\r
193                     log.info("building AccessControl provider of type %s...",type.get());\r
194                     m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(type.get(),acl);\r
195                 }\r
196             }\r
197         }\r
198     }\r
199     catch (exception& ex) {\r
200         log.crit("exception building AccessControl provider: %s", ex.what());\r
201         m_acl = new AccessControlDummy();\r
202     }\r
203 }\r
204 \r
205 Override::Override(const DOMElement* e, Category& log, const Override* base) : m_base(base), m_acl(NULL)\r
206 {\r
207     try {\r
208         // Load the property set.\r
209         load(e,log,this);\r
210         \r
211         // Load any AccessControl provider.\r
212         loadACL(e,log);\r
213     \r
214         // Handle nested Paths.\r
215         DOMElement* path = XMLHelper::getFirstChildElement(e,Path);\r
216         for (int i=1; path; ++i, path=XMLHelper::getNextSiblingElement(path,Path)) {\r
217             const XMLCh* n=path->getAttributeNS(NULL,name);\r
218             \r
219             // Skip any leading slashes.\r
220             while (n && *n==chForwardSlash)\r
221                 n++;\r
222             \r
223             // Check for empty name.\r
224             if (!n || !*n) {\r
225                 log.warn("skipping Path element (%d) with empty name attribute", i);\r
226                 continue;\r
227             }\r
228 \r
229             // Check for an embedded slash.\r
230             int slash=XMLString::indexOf(n,chForwardSlash);\r
231             if (slash>0) {\r
232                 // Copy the first path segment.\r
233                 XMLCh* namebuf=new XMLCh[slash + 1];\r
234                 for (int pos=0; pos < slash; pos++)\r
235                     namebuf[pos]=n[pos];\r
236                 namebuf[slash]=chNull;\r
237                 \r
238                 // Move past the slash in the original pathname.\r
239                 n=n+slash+1;\r
240                 \r
241                 // Skip any leading slashes again.\r
242                 while (*n==chForwardSlash)\r
243                     n++;\r
244                 \r
245                 if (*n) {\r
246                     // Create a placeholder Path element for the first path segment and replant under it.\r
247                     DOMElement* newpath=path->getOwnerDocument()->createElementNS(shibspconstants::SHIB1SPCONFIG_NS,Path);\r
248                     newpath->setAttributeNS(NULL,name,namebuf);\r
249                     path->setAttributeNS(NULL,name,n);\r
250                     path->getParentNode()->replaceChild(newpath,path);\r
251                     newpath->appendChild(path);\r
252                     \r
253                     // Repoint our locals at the new parent.\r
254                     path=newpath;\r
255                     n=path->getAttributeNS(NULL,name);\r
256                 }\r
257                 else {\r
258                     // All we had was a pathname with trailing slash(es), so just reset it without them.\r
259                     path->setAttributeNS(NULL,name,namebuf);\r
260                     n=path->getAttributeNS(NULL,name);\r
261                 }\r
262                 delete[] namebuf;\r
263             }\r
264             \r
265             Override* o=new Override(path,log,this);\r
266             pair<bool,const char*> name=o->getString("name");\r
267             char* dup=strdup(name.second);\r
268             for (char* pch=dup; *pch; pch++)\r
269                 *pch=tolower(*pch);\r
270             if (m_map.count(dup)) {\r
271                 log.warn("Skipping duplicate Path element (%s)",dup);\r
272                 free(dup);\r
273                 delete o;\r
274                 continue;\r
275             }\r
276             m_map[dup]=o;\r
277             free(dup);\r
278         }\r
279     }\r
280     catch (exception&) {\r
281         delete m_acl;\r
282         for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());\r
283         throw;\r
284     }\r
285 }\r
286 \r
287 Override::~Override()\r
288 {\r
289     delete m_acl;\r
290     for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());\r
291 }\r
292 \r
293 pair<bool,bool> Override::getBool(const char* name, const char* ns) const\r
294 {\r
295     pair<bool,bool> ret=DOMPropertySet::getBool(name,ns);\r
296     if (ret.first)\r
297         return ret;\r
298     return m_base ? m_base->getBool(name,ns) : ret;\r
299 }\r
300 \r
301 pair<bool,const char*> Override::getString(const char* name, const char* ns) const\r
302 {\r
303     pair<bool,const char*> ret=DOMPropertySet::getString(name,ns);\r
304     if (ret.first)\r
305         return ret;\r
306     return m_base ? m_base->getString(name,ns) : ret;\r
307 }\r
308 \r
309 pair<bool,const XMLCh*> Override::getXMLString(const char* name, const char* ns) const\r
310 {\r
311     pair<bool,const XMLCh*> ret=DOMPropertySet::getXMLString(name,ns);\r
312     if (ret.first)\r
313         return ret;\r
314     return m_base ? m_base->getXMLString(name,ns) : ret;\r
315 }\r
316 \r
317 pair<bool,unsigned int> Override::getUnsignedInt(const char* name, const char* ns) const\r
318 {\r
319     pair<bool,unsigned int> ret=DOMPropertySet::getUnsignedInt(name,ns);\r
320     if (ret.first)\r
321         return ret;\r
322     return m_base ? m_base->getUnsignedInt(name,ns) : ret;\r
323 }\r
324 \r
325 pair<bool,int> Override::getInt(const char* name, const char* ns) const\r
326 {\r
327     pair<bool,int> ret=DOMPropertySet::getInt(name,ns);\r
328     if (ret.first)\r
329         return ret;\r
330     return m_base ? m_base->getInt(name,ns) : ret;\r
331 }\r
332 \r
333 const PropertySet* Override::getPropertySet(const char* name, const char* ns) const\r
334 {\r
335     const PropertySet* ret=DOMPropertySet::getPropertySet(name,ns);\r
336     if (ret || !m_base)\r
337         return ret;\r
338     return m_base->getPropertySet(name,ns);\r
339 }\r
340 \r
341 const Override* Override::locate(const char* path) const\r
342 {\r
343     char* dup=strdup(path);\r
344     char* sep=strchr(dup,'?');\r
345     if (sep)\r
346         *sep=0;\r
347     for (char* pch=dup; *pch; pch++)\r
348         *pch=tolower(*pch);\r
349         \r
350     const Override* o=this;\r
351     \r
352 #ifdef HAVE_STRTOK_R\r
353     char* pos=NULL;\r
354     const char* token=strtok_r(dup,"/",&pos);\r
355 #else\r
356     const char* token=strtok(dup,"/");\r
357 #endif\r
358     while (token)\r
359     {\r
360         map<string,Override*>::const_iterator i=o->m_map.find(token);\r
361         if (i==o->m_map.end())\r
362             break;\r
363         o=i->second;\r
364 #ifdef HAVE_STRTOK_R\r
365         token=strtok_r(NULL,"/",&pos);\r
366 #else\r
367         token=strtok(NULL,"/");\r
368 #endif\r
369     }\r
370 \r
371     free(dup);\r
372     return o;\r
373 }\r
374 \r
375 XMLRequestMapperImpl::XMLRequestMapperImpl(const DOMElement* e, Category& log) : m_document(NULL)\r
376 {\r
377 #ifdef _DEBUG\r
378     xmltooling::NDC ndc("XMLRequestMapperImpl");\r
379 #endif\r
380 \r
381     // Load the property set.\r
382     load(e,log,this);\r
383     \r
384     // Load any AccessControl provider.\r
385     loadACL(e,log);\r
386 \r
387     // Loop over the Host elements.\r
388     const DOMElement* host = XMLHelper::getFirstChildElement(e,Host);\r
389     for (int i=1; host; ++i, host=XMLHelper::getNextSiblingElement(host,Host)) {\r
390         const XMLCh* n=host->getAttributeNS(NULL,name);\r
391         if (!n || !*n) {\r
392             log.warn("Skipping Host element (%d) with empty name attribute",i);\r
393             continue;\r
394         }\r
395         \r
396         Override* o=new Override(host,log,this);\r
397         pair<bool,const char*> name=o->getString("name");\r
398         pair<bool,const char*> scheme=o->getString("scheme");\r
399         pair<bool,const char*> port=o->getString("port");\r
400         \r
401         char* dup=strdup(name.second);\r
402         for (char* pch=dup; *pch; pch++)\r
403             *pch=tolower(*pch);\r
404         auto_ptr<char> dupwrap(dup);\r
405 \r
406         if (!scheme.first && port.first) {\r
407             // No scheme, but a port, so assume http.\r
408             scheme = pair<bool,const char*>(true,"http");\r
409         }\r
410         else if (scheme.first && !port.first) {\r
411             // Scheme, no port, so default it.\r
412             // XXX Use getservbyname instead?\r
413             port.first = true;\r
414             if (!strcmp(scheme.second,"http"))\r
415                 port.second = "80";\r
416             else if (!strcmp(scheme.second,"https"))\r
417                 port.second = "443";\r
418             else if (!strcmp(scheme.second,"ftp"))\r
419                 port.second = "21";\r
420             else if (!strcmp(scheme.second,"ldap"))\r
421                 port.second = "389";\r
422             else if (!strcmp(scheme.second,"ldaps"))\r
423                 port.second = "636";\r
424         }\r
425 \r
426         if (scheme.first) {\r
427             string url(scheme.second);\r
428             url=url + "://" + dup;\r
429             \r
430             // Is this the default port?\r
431             if ((!strcmp(scheme.second,"http") && !strcmp(port.second,"80")) ||\r
432                 (!strcmp(scheme.second,"https") && !strcmp(port.second,"443")) ||\r
433                 (!strcmp(scheme.second,"ftp") && !strcmp(port.second,"21")) ||\r
434                 (!strcmp(scheme.second,"ldap") && !strcmp(port.second,"389")) ||\r
435                 (!strcmp(scheme.second,"ldaps") && !strcmp(port.second,"636"))) {\r
436                 // First store a port-less version.\r
437                 if (m_map.count(url) || m_extras.count(url)) {\r
438                     log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
439                     delete o;\r
440                     continue;\r
441                 }\r
442                 m_map[url]=o;\r
443                 log.debug("Added <Host> mapping for %s",url.c_str());\r
444                 \r
445                 // Now append the port. We use the extras vector, to avoid double freeing the object later.\r
446                 url=url + ':' + port.second;\r
447                 m_extras[url]=o;\r
448                 log.debug("Added <Host> mapping for %s",url.c_str());\r
449             }\r
450             else {\r
451                 url=url + ':' + port.second;\r
452                 if (m_map.count(url) || m_extras.count(url)) {\r
453                     log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
454                     delete o;\r
455                     continue;\r
456                 }\r
457                 m_map[url]=o;\r
458                 log.debug("Added <Host> mapping for %s",url.c_str());\r
459             }\r
460         }\r
461         else {\r
462             // No scheme or port, so we enter dual hosts on http:80 and https:443\r
463             string url("http://");\r
464             url = url + dup;\r
465             if (m_map.count(url) || m_extras.count(url)) {\r
466                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
467                 delete o;\r
468                 continue;\r
469             }\r
470             m_map[url]=o;\r
471             log.debug("Added <Host> mapping for %s",url.c_str());\r
472             \r
473             url = url + ":80";\r
474             if (m_map.count(url) || m_extras.count(url)) {\r
475                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
476                 continue;\r
477             }\r
478             m_extras[url]=o;\r
479             log.debug("Added <Host> mapping for %s",url.c_str());\r
480             \r
481             url = "https://";\r
482             url = url + dup;\r
483             if (m_map.count(url) || m_extras.count(url)) {\r
484                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
485                 continue;\r
486             }\r
487             m_extras[url]=o;\r
488             log.debug("Added <Host> mapping for %s",url.c_str());\r
489             \r
490             url = url + ":443";\r
491             if (m_map.count(url) || m_extras.count(url)) {\r
492                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
493                 continue;\r
494             }\r
495             m_extras[url]=o;\r
496             log.debug("Added <Host> mapping for %s",url.c_str());\r
497         }\r
498     }\r
499 }\r
500 \r
501 const Override* XMLRequestMapperImpl::findOverride(const char* vhost, const char* path) const\r
502 {\r
503     const Override* o=NULL;\r
504     map<string,Override*>::const_iterator i=m_map.find(vhost);\r
505     if (i!=m_map.end())\r
506         o=i->second;\r
507     else {\r
508         i=m_extras.find(vhost);\r
509         if (i!=m_extras.end())\r
510             o=i->second;\r
511     }\r
512     \r
513     return o ? o->locate(path) : this;\r
514 }\r
515 \r
516 pair<bool,DOMElement*> XMLRequestMapper::load()\r
517 {\r
518     // Load from source using base class.\r
519     pair<bool,DOMElement*> raw = ReloadableXMLFile::load();\r
520     \r
521     // If we own it, wrap it.\r
522     XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : NULL);\r
523 \r
524     XMLRequestMapperImpl* impl = new XMLRequestMapperImpl(raw.second,m_log);\r
525     \r
526     // If we held the document, transfer it to the impl. If we didn't, it's a no-op.\r
527     impl->setDocument(docjanitor.release());\r
528 \r
529     delete m_impl;\r
530     m_impl = impl;\r
531 \r
532     return make_pair(false,(DOMElement*)NULL);\r
533 }\r
534 \r
535 RequestMapper::Settings XMLRequestMapper::getSettings(const SPRequest& request) const\r
536 {\r
537     ostringstream vhost;\r
538     vhost << request.getScheme() << "://" << request.getHostname() << ':' << request.getPort();\r
539 \r
540     const Override* o=m_impl->findOverride(vhost.str().c_str(), request.getRequestURI());\r
541 \r
542     if (m_log.isDebugEnabled()) {\r
543 #ifdef _DEBUG\r
544         xmltooling::NDC ndc("getSettings");\r
545 #endif\r
546         pair<bool,const char*> ret=o->getString("applicationId");\r
547         m_log.debug("mapped %s%s to %s", vhost.str().c_str(), request.getRequestURI() ? request.getRequestURI() : "", ret.second);\r
548     }\r
549 \r
550     return Settings(o,o->getAC());\r
551 }\r