772c51ca5888d92c7929649fe0645735e575259f
[shibboleth/sp.git] / shibsp / impl / XMLRequestMapper.cpp
1 /*\r
2  *  Copyright 2001-2007 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("edu.internet2.middleware.shibboleth.sp.provider.XMLRequestMapProvider", XMLRequestMapperFactory);\r
158     conf.RequestMapperManager.registerFactory("edu.internet2.middleware.shibboleth.target.provider.XMLRequestMap", XMLRequestMapperFactory);\r
159     conf.RequestMapperManager.registerFactory(NATIVE_REQUEST_MAPPER, XMLRequestMapperFactory);\r
160     conf.RequestMapperManager.registerFactory("edu.internet2.middleware.shibboleth.sp.provider.NativeRequestMapProvider", XMLRequestMapperFactory);\r
161 }\r
162 \r
163 short Override::acceptNode(const DOMNode* node) const\r
164 {\r
165     if (!XMLString::equals(node->getNamespaceURI(),shibspconstants::SHIB1SPCONFIG_NS))\r
166         return FILTER_ACCEPT;\r
167     const XMLCh* name=node->getLocalName();\r
168     if (XMLString::equals(name,Host) ||\r
169         XMLString::equals(name,Path) ||\r
170         XMLString::equals(name,_AccessControl) ||\r
171         XMLString::equals(name,htaccess) ||\r
172         XMLString::equals(name,AccessControlProvider))\r
173         return FILTER_REJECT;\r
174 \r
175     return FILTER_ACCEPT;\r
176 }\r
177 \r
178 void Override::loadACL(const DOMElement* e, Category& log)\r
179 {\r
180     try {\r
181         const DOMElement* acl=XMLHelper::getFirstChildElement(e,htaccess);\r
182         if (acl) {\r
183             log.info("building Apache htaccess AccessControl provider...");\r
184             m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(HT_ACCESS_CONTROL,acl);\r
185         }\r
186         else {\r
187             acl=XMLHelper::getFirstChildElement(e,_AccessControl);\r
188             if (acl) {\r
189                 log.info("building XML-based AccessControl provider...");\r
190                 m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(XML_ACCESS_CONTROL,acl);\r
191             }\r
192             else {\r
193                 acl=XMLHelper::getFirstChildElement(e,AccessControlProvider);\r
194                 if (acl) {\r
195                     xmltooling::auto_ptr_char type(acl->getAttributeNS(NULL,type));\r
196                     log.info("building AccessControl provider of type %s...",type.get());\r
197                     m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(type.get(),acl);\r
198                 }\r
199             }\r
200         }\r
201     }\r
202     catch (exception& ex) {\r
203         log.crit("exception building AccessControl provider: %s", ex.what());\r
204         m_acl = new AccessControlDummy();\r
205     }\r
206 }\r
207 \r
208 Override::Override(const DOMElement* e, Category& log, const Override* base) : m_base(base), m_acl(NULL)\r
209 {\r
210     try {\r
211         // Load the property set.\r
212         load(e,log,this);\r
213         \r
214         // Load any AccessControl provider.\r
215         loadACL(e,log);\r
216     \r
217         // Handle nested Paths.\r
218         DOMElement* path = XMLHelper::getFirstChildElement(e,Path);\r
219         for (int i=1; path; ++i, path=XMLHelper::getNextSiblingElement(path,Path)) {\r
220             const XMLCh* n=path->getAttributeNS(NULL,name);\r
221             \r
222             // Skip any leading slashes.\r
223             while (n && *n==chForwardSlash)\r
224                 n++;\r
225             \r
226             // Check for empty name.\r
227             if (!n || !*n) {\r
228                 log.warn("skipping Path element (%d) with empty name attribute", i);\r
229                 continue;\r
230             }\r
231 \r
232             // Check for an embedded slash.\r
233             int slash=XMLString::indexOf(n,chForwardSlash);\r
234             if (slash>0) {\r
235                 // Copy the first path segment.\r
236                 XMLCh* namebuf=new XMLCh[slash + 1];\r
237                 for (int pos=0; pos < slash; pos++)\r
238                     namebuf[pos]=n[pos];\r
239                 namebuf[slash]=chNull;\r
240                 \r
241                 // Move past the slash in the original pathname.\r
242                 n=n+slash+1;\r
243                 \r
244                 // Skip any leading slashes again.\r
245                 while (*n==chForwardSlash)\r
246                     n++;\r
247                 \r
248                 if (*n) {\r
249                     // Create a placeholder Path element for the first path segment and replant under it.\r
250                     DOMElement* newpath=path->getOwnerDocument()->createElementNS(shibspconstants::SHIB1SPCONFIG_NS,Path);\r
251                     newpath->setAttributeNS(NULL,name,namebuf);\r
252                     path->setAttributeNS(NULL,name,n);\r
253                     path->getParentNode()->replaceChild(newpath,path);\r
254                     newpath->appendChild(path);\r
255                     \r
256                     // Repoint our locals at the new parent.\r
257                     path=newpath;\r
258                     n=path->getAttributeNS(NULL,name);\r
259                 }\r
260                 else {\r
261                     // All we had was a pathname with trailing slash(es), so just reset it without them.\r
262                     path->setAttributeNS(NULL,name,namebuf);\r
263                     n=path->getAttributeNS(NULL,name);\r
264                 }\r
265                 delete[] namebuf;\r
266             }\r
267             \r
268             Override* o=new Override(path,log,this);\r
269             pair<bool,const char*> name=o->getString("name");\r
270             char* dup=strdup(name.second);\r
271             for (char* pch=dup; *pch; pch++)\r
272                 *pch=tolower(*pch);\r
273             if (m_map.count(dup)) {\r
274                 log.warn("Skipping duplicate Path element (%s)",dup);\r
275                 free(dup);\r
276                 delete o;\r
277                 continue;\r
278             }\r
279             m_map[dup]=o;\r
280             free(dup);\r
281         }\r
282     }\r
283     catch (exception&) {\r
284         delete m_acl;\r
285         for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());\r
286         throw;\r
287     }\r
288 }\r
289 \r
290 Override::~Override()\r
291 {\r
292     delete m_acl;\r
293     for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());\r
294 }\r
295 \r
296 pair<bool,bool> Override::getBool(const char* name, const char* ns) const\r
297 {\r
298     pair<bool,bool> ret=DOMPropertySet::getBool(name,ns);\r
299     if (ret.first)\r
300         return ret;\r
301     return m_base ? m_base->getBool(name,ns) : ret;\r
302 }\r
303 \r
304 pair<bool,const char*> Override::getString(const char* name, const char* ns) const\r
305 {\r
306     pair<bool,const char*> ret=DOMPropertySet::getString(name,ns);\r
307     if (ret.first)\r
308         return ret;\r
309     return m_base ? m_base->getString(name,ns) : ret;\r
310 }\r
311 \r
312 pair<bool,const XMLCh*> Override::getXMLString(const char* name, const char* ns) const\r
313 {\r
314     pair<bool,const XMLCh*> ret=DOMPropertySet::getXMLString(name,ns);\r
315     if (ret.first)\r
316         return ret;\r
317     return m_base ? m_base->getXMLString(name,ns) : ret;\r
318 }\r
319 \r
320 pair<bool,unsigned int> Override::getUnsignedInt(const char* name, const char* ns) const\r
321 {\r
322     pair<bool,unsigned int> ret=DOMPropertySet::getUnsignedInt(name,ns);\r
323     if (ret.first)\r
324         return ret;\r
325     return m_base ? m_base->getUnsignedInt(name,ns) : ret;\r
326 }\r
327 \r
328 pair<bool,int> Override::getInt(const char* name, const char* ns) const\r
329 {\r
330     pair<bool,int> ret=DOMPropertySet::getInt(name,ns);\r
331     if (ret.first)\r
332         return ret;\r
333     return m_base ? m_base->getInt(name,ns) : ret;\r
334 }\r
335 \r
336 const PropertySet* Override::getPropertySet(const char* name, const char* ns) const\r
337 {\r
338     const PropertySet* ret=DOMPropertySet::getPropertySet(name,ns);\r
339     if (ret || !m_base)\r
340         return ret;\r
341     return m_base->getPropertySet(name,ns);\r
342 }\r
343 \r
344 const Override* Override::locate(const char* path) const\r
345 {\r
346     char* dup=strdup(path);\r
347     char* sep=strchr(dup,'?');\r
348     if (sep)\r
349         *sep=0;\r
350     for (char* pch=dup; *pch; pch++)\r
351         *pch=tolower(*pch);\r
352         \r
353     const Override* o=this;\r
354     \r
355 #ifdef HAVE_STRTOK_R\r
356     char* pos=NULL;\r
357     const char* token=strtok_r(dup,"/",&pos);\r
358 #else\r
359     const char* token=strtok(dup,"/");\r
360 #endif\r
361     while (token)\r
362     {\r
363         map<string,Override*>::const_iterator i=o->m_map.find(token);\r
364         if (i==o->m_map.end())\r
365             break;\r
366         o=i->second;\r
367 #ifdef HAVE_STRTOK_R\r
368         token=strtok_r(NULL,"/",&pos);\r
369 #else\r
370         token=strtok(NULL,"/");\r
371 #endif\r
372     }\r
373 \r
374     free(dup);\r
375     return o;\r
376 }\r
377 \r
378 XMLRequestMapperImpl::XMLRequestMapperImpl(const DOMElement* e, Category& log) : m_document(NULL)\r
379 {\r
380 #ifdef _DEBUG\r
381     xmltooling::NDC ndc("XMLRequestMapperImpl");\r
382 #endif\r
383 \r
384     // Load the property set.\r
385     load(e,log,this);\r
386     \r
387     // Load any AccessControl provider.\r
388     loadACL(e,log);\r
389 \r
390     // Loop over the Host elements.\r
391     const DOMElement* host = XMLHelper::getFirstChildElement(e,Host);\r
392     for (int i=1; host; ++i, host=XMLHelper::getNextSiblingElement(host,Host)) {\r
393         const XMLCh* n=host->getAttributeNS(NULL,name);\r
394         if (!n || !*n) {\r
395             log.warn("Skipping Host element (%d) with empty name attribute",i);\r
396             continue;\r
397         }\r
398         \r
399         Override* o=new Override(host,log,this);\r
400         pair<bool,const char*> name=o->getString("name");\r
401         pair<bool,const char*> scheme=o->getString("scheme");\r
402         pair<bool,const char*> port=o->getString("port");\r
403         \r
404         char* dup=strdup(name.second);\r
405         for (char* pch=dup; *pch; pch++)\r
406             *pch=tolower(*pch);\r
407         auto_ptr<char> dupwrap(dup);\r
408 \r
409         if (!scheme.first && port.first) {\r
410             // No scheme, but a port, so assume http.\r
411             scheme = pair<bool,const char*>(true,"http");\r
412         }\r
413         else if (scheme.first && !port.first) {\r
414             // Scheme, no port, so default it.\r
415             // XXX Use getservbyname instead?\r
416             port.first = true;\r
417             if (!strcmp(scheme.second,"http"))\r
418                 port.second = "80";\r
419             else if (!strcmp(scheme.second,"https"))\r
420                 port.second = "443";\r
421             else if (!strcmp(scheme.second,"ftp"))\r
422                 port.second = "21";\r
423             else if (!strcmp(scheme.second,"ldap"))\r
424                 port.second = "389";\r
425             else if (!strcmp(scheme.second,"ldaps"))\r
426                 port.second = "636";\r
427         }\r
428 \r
429         if (scheme.first) {\r
430             string url(scheme.second);\r
431             url=url + "://" + dup;\r
432             \r
433             // Is this the default port?\r
434             if ((!strcmp(scheme.second,"http") && !strcmp(port.second,"80")) ||\r
435                 (!strcmp(scheme.second,"https") && !strcmp(port.second,"443")) ||\r
436                 (!strcmp(scheme.second,"ftp") && !strcmp(port.second,"21")) ||\r
437                 (!strcmp(scheme.second,"ldap") && !strcmp(port.second,"389")) ||\r
438                 (!strcmp(scheme.second,"ldaps") && !strcmp(port.second,"636"))) {\r
439                 // First store a port-less version.\r
440                 if (m_map.count(url) || m_extras.count(url)) {\r
441                     log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
442                     delete o;\r
443                     continue;\r
444                 }\r
445                 m_map[url]=o;\r
446                 log.debug("Added <Host> mapping for %s",url.c_str());\r
447                 \r
448                 // Now append the port. We use the extras vector, to avoid double freeing the object later.\r
449                 url=url + ':' + port.second;\r
450                 m_extras[url]=o;\r
451                 log.debug("Added <Host> mapping for %s",url.c_str());\r
452             }\r
453             else {\r
454                 url=url + ':' + port.second;\r
455                 if (m_map.count(url) || m_extras.count(url)) {\r
456                     log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
457                     delete o;\r
458                     continue;\r
459                 }\r
460                 m_map[url]=o;\r
461                 log.debug("Added <Host> mapping for %s",url.c_str());\r
462             }\r
463         }\r
464         else {\r
465             // No scheme or port, so we enter dual hosts on http:80 and https:443\r
466             string url("http://");\r
467             url = url + dup;\r
468             if (m_map.count(url) || m_extras.count(url)) {\r
469                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
470                 delete o;\r
471                 continue;\r
472             }\r
473             m_map[url]=o;\r
474             log.debug("Added <Host> mapping for %s",url.c_str());\r
475             \r
476             url = url + ":80";\r
477             if (m_map.count(url) || m_extras.count(url)) {\r
478                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
479                 continue;\r
480             }\r
481             m_extras[url]=o;\r
482             log.debug("Added <Host> mapping for %s",url.c_str());\r
483             \r
484             url = "https://";\r
485             url = url + dup;\r
486             if (m_map.count(url) || m_extras.count(url)) {\r
487                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
488                 continue;\r
489             }\r
490             m_extras[url]=o;\r
491             log.debug("Added <Host> mapping for %s",url.c_str());\r
492             \r
493             url = url + ":443";\r
494             if (m_map.count(url) || m_extras.count(url)) {\r
495                 log.warn("Skipping duplicate Host element (%s)",url.c_str());\r
496                 continue;\r
497             }\r
498             m_extras[url]=o;\r
499             log.debug("Added <Host> mapping for %s",url.c_str());\r
500         }\r
501     }\r
502 }\r
503 \r
504 const Override* XMLRequestMapperImpl::findOverride(const char* vhost, const char* path) const\r
505 {\r
506     const Override* o=NULL;\r
507     map<string,Override*>::const_iterator i=m_map.find(vhost);\r
508     if (i!=m_map.end())\r
509         o=i->second;\r
510     else {\r
511         i=m_extras.find(vhost);\r
512         if (i!=m_extras.end())\r
513             o=i->second;\r
514     }\r
515     \r
516     return o ? o->locate(path) : this;\r
517 }\r
518 \r
519 pair<bool,DOMElement*> XMLRequestMapper::load()\r
520 {\r
521     // Load from source using base class.\r
522     pair<bool,DOMElement*> raw = ReloadableXMLFile::load();\r
523     \r
524     // If we own it, wrap it.\r
525     XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : NULL);\r
526 \r
527     XMLRequestMapperImpl* impl = new XMLRequestMapperImpl(raw.second,m_log);\r
528     \r
529     // If we held the document, transfer it to the impl. If we didn't, it's a no-op.\r
530     impl->setDocument(docjanitor.release());\r
531 \r
532     delete m_impl;\r
533     m_impl = impl;\r
534 \r
535     return make_pair(false,(DOMElement*)NULL);\r
536 }\r
537 \r
538 RequestMapper::Settings XMLRequestMapper::getSettings(const SPRequest& request) const\r
539 {\r
540     ostringstream vhost;\r
541     vhost << request.getScheme() << "://" << request.getHostname() << ':' << request.getPort();\r
542 \r
543     const Override* o=m_impl->findOverride(vhost.str().c_str(), request.getRequestURI());\r
544 \r
545     if (m_log.isDebugEnabled()) {\r
546 #ifdef _DEBUG\r
547         xmltooling::NDC ndc("getSettings");\r
548 #endif\r
549         pair<bool,const char*> ret=o->getString("applicationId");\r
550         m_log.debug("mapped %s%s to %s", vhost.str().c_str(), request.getRequestURI() ? request.getRequestURI() : "", ret.second);\r
551     }\r
552 \r
553     return Settings(o,o->getAC());\r
554 }\r