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