2 * Copyright 2001-2010 Internet2
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 /** XMLRequestMapper.cpp
19 * XML-based RequestMapper implementation.
23 #include "exceptions.h"
24 #include "AccessControl.h"
25 #include "RequestMapper.h"
26 #include "SPRequest.h"
27 #include "util/DOMPropertySet.h"
28 #include "util/SPConstants.h"
31 #include <xmltooling/util/NDC.h>
32 #include <xmltooling/util/ReloadableXMLFile.h>
33 #include <xmltooling/util/Threads.h>
34 #include <xmltooling/util/XMLHelper.h>
35 #include <xercesc/util/XMLUniDefs.hpp>
36 #include <xercesc/util/regx/RegularExpression.hpp>
38 using shibspconstants::SHIB2SPCONFIG_NS;
39 using namespace shibsp;
40 using namespace xmltooling;
45 // Blocks access when an ACL plugin fails to load.
46 class AccessControlDummy : public AccessControl
55 aclresult_t authorized(const SPRequest& request, const Session* session) const {
56 return shib_acl_false;
60 class Override : public DOMPropertySet, public DOMNodeFilter
63 Override(bool unicodeAware=false) : m_unicodeAware(unicodeAware), m_acl(nullptr) {}
64 Override(bool unicodeAware, const DOMElement* e, Category& log, const Override* base=nullptr);
67 // Provides filter to exclude special config elements.
68 #ifdef SHIBSP_XERCESC_SHORT_ACCEPTNODE
73 acceptNode(const DOMNode* node) const {
77 const Override* locate(const HTTPRequest& request) const;
78 AccessControl* getAC() const { return (m_acl ? m_acl : (getParent() ? dynamic_cast<const Override*>(getParent())->getAC() : nullptr)); }
81 void loadACL(const DOMElement* e, Category& log);
84 map<string,Override*> m_map;
85 vector< pair<RegularExpression*,Override*> > m_regexps;
86 vector< pair< pair<string,RegularExpression*>,Override*> > m_queries;
92 class XMLRequestMapperImpl : public Override
95 XMLRequestMapperImpl(const DOMElement* e, Category& log);
97 ~XMLRequestMapperImpl() {
99 m_document->release();
102 void setDocument(DOMDocument* doc) {
106 const Override* findOverride(const char* vhost, const HTTPRequest& request) const;
109 map<string,Override*> m_extras;
110 DOMDocument* m_document;
113 #if defined (_MSC_VER)
114 #pragma warning( push )
115 #pragma warning( disable : 4250 )
118 class XMLRequestMapper : public RequestMapper, public ReloadableXMLFile
121 XMLRequestMapper(const DOMElement* e) : ReloadableXMLFile(e,Category::getInstance(SHIBSP_LOGCAT".RequestMapper")), m_impl(nullptr) {
125 ~XMLRequestMapper() {
130 Settings getSettings(const HTTPRequest& request) const;
133 pair<bool,DOMElement*> background_load();
136 XMLRequestMapperImpl* m_impl;
139 #if defined (_MSC_VER)
140 #pragma warning( pop )
143 RequestMapper* SHIBSP_DLLLOCAL XMLRequestMapperFactory(const DOMElement* const & e)
145 return new XMLRequestMapper(e);
148 static const XMLCh _AccessControl[] = UNICODE_LITERAL_13(A,c,c,e,s,s,C,o,n,t,r,o,l);
149 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);
150 static const XMLCh Host[] = UNICODE_LITERAL_4(H,o,s,t);
151 static const XMLCh HostRegex[] = UNICODE_LITERAL_9(H,o,s,t,R,e,g,e,x);
152 static const XMLCh htaccess[] = UNICODE_LITERAL_8(h,t,a,c,c,e,s,s);
153 static const XMLCh ignoreCase[] = UNICODE_LITERAL_10(i,g,n,o,r,e,C,a,s,e);
154 static const XMLCh ignoreOption[] = UNICODE_LITERAL_1(i);
155 static const XMLCh Path[] = UNICODE_LITERAL_4(P,a,t,h);
156 static const XMLCh PathRegex[] = UNICODE_LITERAL_9(P,a,t,h,R,e,g,e,x);
157 static const XMLCh Query[] = UNICODE_LITERAL_5(Q,u,e,r,y);
158 static const XMLCh name[] = UNICODE_LITERAL_4(n,a,m,e);
159 static const XMLCh regex[] = UNICODE_LITERAL_5(r,e,g,e,x);
160 static const XMLCh _type[] = UNICODE_LITERAL_4(t,y,p,e);
163 void SHIBSP_API shibsp::registerRequestMappers()
165 SPConfig& conf=SPConfig::getConfig();
166 conf.RequestMapperManager.registerFactory(XML_REQUEST_MAPPER, XMLRequestMapperFactory);
167 conf.RequestMapperManager.registerFactory(NATIVE_REQUEST_MAPPER, XMLRequestMapperFactory);
170 RequestMapper::RequestMapper()
174 RequestMapper::~RequestMapper()
178 void Override::loadACL(const DOMElement* e, Category& log)
181 const DOMElement* acl=XMLHelper::getFirstChildElement(e,htaccess);
183 log.info("building Apache htaccess AccessControl provider...");
184 m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(HT_ACCESS_CONTROL,acl);
187 acl=XMLHelper::getFirstChildElement(e,_AccessControl);
189 log.info("building XML-based AccessControl provider...");
190 m_acl=SPConfig::getConfig().AccessControlManager.newPlugin(XML_ACCESS_CONTROL,acl);
193 acl=XMLHelper::getFirstChildElement(e,AccessControlProvider);
195 string t(XMLHelper::getAttrString(acl, nullptr, _type));
197 log.info("building AccessControl provider of type %s...", t.c_str());
198 m_acl = SPConfig::getConfig().AccessControlManager.newPlugin(t.c_str(), acl);
201 throw ConfigurationException("<AccessControlProvider> missing type attribute.");
207 catch (exception& ex) {
208 log.crit("exception building AccessControl provider: %s", ex.what());
209 m_acl = new AccessControlDummy();
213 Override::Override(bool unicodeAware, const DOMElement* e, Category& log, const Override* base)
214 : m_unicodeAware(unicodeAware), m_acl(nullptr)
217 // Load the property set.
218 load(e,nullptr,this);
221 // Load any AccessControl provider.
224 // Handle nested Paths.
225 DOMElement* path = XMLHelper::getFirstChildElement(e,Path);
226 for (int i=1; path; ++i, path=XMLHelper::getNextSiblingElement(path,Path)) {
227 const XMLCh* n=path->getAttributeNS(nullptr,name);
229 // Skip any leading slashes.
230 while (n && *n==chForwardSlash)
233 // Check for empty name.
235 log.warn("skipping Path element (%d) with empty name attribute", i);
239 // Check for an embedded slash.
240 int slash=XMLString::indexOf(n,chForwardSlash);
242 // Copy the first path segment.
243 XMLCh* namebuf=new XMLCh[slash + 1];
244 for (int pos=0; pos < slash; pos++)
246 namebuf[slash]=chNull;
248 // Move past the slash in the original pathname.
251 // Skip any leading slashes again.
252 while (*n==chForwardSlash)
256 // Create a placeholder Path element for the first path segment and replant under it.
257 DOMElement* newpath=path->getOwnerDocument()->createElementNS(shibspconstants::SHIB2SPCONFIG_NS,Path);
258 newpath->setAttributeNS(nullptr,name,namebuf);
259 path->setAttributeNS(nullptr,name,n);
260 path->getParentNode()->replaceChild(newpath,path);
261 newpath->appendChild(path);
263 // Repoint our locals at the new parent.
265 n=path->getAttributeNS(nullptr,name);
268 // All we had was a pathname with trailing slash(es), so just reset it without them.
269 path->setAttributeNS(nullptr,name,namebuf);
270 n=path->getAttributeNS(nullptr,name);
276 Override* o = new Override(m_unicodeAware, path, log, this);
277 if (m_unicodeAware) {
278 dup = toUTF8(o->getXMLString("name").second, true /* use malloc */);
281 dup = strdup(o->getString("name").second);
282 for (char* pch = dup; *pch; ++pch)
283 *pch = tolower(*pch);
285 if (m_map.count(dup)) {
286 log.warn("skipping duplicate Path element (%s)",dup);
292 log.debug("added Path mapping (%s)", dup);
296 if (!XMLString::equals(e->getLocalName(), PathRegex)) {
297 // Handle nested PathRegexs.
298 path = XMLHelper::getFirstChildElement(e,PathRegex);
299 for (int i=1; path; ++i, path=XMLHelper::getNextSiblingElement(path,PathRegex)) {
300 const XMLCh* n=path->getAttributeNS(nullptr,regex);
302 log.warn("skipping PathRegex element (%d) with empty regex attribute",i);
306 auto_ptr<Override> o(new Override(m_unicodeAware, path, log, this));
308 const XMLCh* flag=path->getAttributeNS(nullptr,ignoreCase);
310 auto_ptr<RegularExpression> re(
311 new RegularExpression(n, (flag && (*flag==chLatin_f || *flag==chDigit_0)) ? &chNull : ignoreOption)
313 m_regexps.push_back(make_pair(re.release(), o.release()));
315 catch (XMLException& ex) {
316 auto_ptr_char tmp(ex.getMessage());
317 log.error("caught exception while parsing PathRegex regular expression (%d): %s", i, tmp.get());
318 throw ConfigurationException("Invalid regular expression in PathRegex element.");
321 if (log.isDebugEnabled())
322 log.debug("added <PathRegex> mapping (%s)", m_regexps.back().second->getString("regex").second);
326 // Handle nested Querys.
327 path = XMLHelper::getFirstChildElement(e,Query);
328 for (int i=1; path; ++i, path=XMLHelper::getNextSiblingElement(path,Query)) {
329 const XMLCh* n=path->getAttributeNS(nullptr,name);
331 log.warn("skipping Query element (%d) with empty name attribute",i);
334 auto_ptr_char ntemp(n);
335 const XMLCh* v=path->getAttributeNS(nullptr,regex);
337 auto_ptr<Override> o(new Override(m_unicodeAware, path, log, this));
339 RegularExpression* re = nullptr;
341 re = new RegularExpression(v);
342 m_queries.push_back(make_pair(make_pair(string(ntemp.get()),re), o.release()));
344 catch (XMLException& ex) {
345 auto_ptr_char tmp(ex.getMessage());
346 log.error("caught exception while parsing Query regular expression (%d): %s", i, tmp.get());
347 throw ConfigurationException("Invalid regular expression in Query element.");
350 log.debug("added <Query> mapping (%s)", ntemp.get());
355 for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());
356 for (vector< pair<RegularExpression*,Override*> >::iterator i = m_regexps.begin(); i != m_regexps.end(); ++i) {
360 for (vector< pair< pair<string,RegularExpression*>,Override*> >::iterator j = m_queries.begin(); j != m_queries.end(); ++j) {
361 delete j->first.second;
368 Override::~Override()
371 for_each(m_map.begin(),m_map.end(),xmltooling::cleanup_pair<string,Override>());
372 for (vector< pair<RegularExpression*,Override*> >::iterator i = m_regexps.begin(); i != m_regexps.end(); ++i) {
376 for (vector< pair< pair<string,RegularExpression*>,Override*> >::iterator j = m_queries.begin(); j != m_queries.end(); ++j) {
377 delete j->first.second;
382 const Override* Override::locate(const HTTPRequest& request) const
384 // This function is confusing because it's *not* recursive.
385 // The whole path is tokenized and mapped in a loop, so the
386 // path parameter starts with the entire request path and
387 // we can skip the leading slash as irrelevant.
388 const char* path = request.getRequestURI();
392 // Now we copy the path, chop the query string, and possibly lower case it.
393 char* dup=strdup(path);
394 char* sep=strchr(dup,'?');
397 if (!m_unicodeAware) {
398 for (char* pch=dup; *pch; pch++)
402 // Default is for the current object to provide settings.
403 const Override* o=this;
405 // Tokenize the path by segment and try and map each segment.
408 const char* token=strtok_r(dup,"/",&pos);
410 const char* token=strtok(dup,"/");
413 map<string,Override*>::const_iterator i=o->m_map.find(token);
414 if (i==o->m_map.end())
415 break; // Once there's no match, we've consumed as much of the path as possible here.
416 // We found a match, so reset the settings pointer.
419 // We descended a step down the path, so we need to advance the original
420 // parameter for the regex step later.
421 path += strlen(token);
425 // Get the next segment, if any.
427 token=strtok_r(nullptr,"/",&pos);
429 token=strtok(nullptr,"/");
435 // If there's anything left, we try for a regex match on the rest of the path minus the query string.
438 path2 = path2.substr(0,path2.find('?'));
440 for (vector< pair<RegularExpression*,Override*> >::const_iterator re = o->m_regexps.begin(); re != o->m_regexps.end(); ++re) {
441 if (re->first->matches(path2.c_str())) {
448 // Finally, check for query string matches. This is another "unrolled" recursive descent in a loop.
452 for (vector< pair< pair<string,RegularExpression*>,Override*> >::const_iterator q = o->m_queries.begin(); !descended && q != o->m_queries.end(); ++q) {
453 vector<const char*> vals;
454 if (request.getParameters(q->first.first.c_str(), vals)) {
455 if (q->first.second) {
456 // We have to match one of the values.
457 for (vector<const char*>::const_iterator v = vals.begin(); v != vals.end(); ++v) {
458 if (q->first.second->matches(*v)) {
466 // The simple presence of the parameter is sufficient to match.
477 XMLRequestMapperImpl::XMLRequestMapperImpl(const DOMElement* e, Category& log) : m_document(nullptr)
480 xmltooling::NDC ndc("XMLRequestMapperImpl");
482 static const XMLCh _RequestMap[] = UNICODE_LITERAL_10(R,e,q,u,e,s,t,M,a,p);
484 if (e && !XMLHelper::isNodeNamed(e, SHIB2SPCONFIG_NS, _RequestMap))
485 throw ConfigurationException("XML RequestMapper requires conf:RequestMap at root of configuration.");
487 // Load the property set.
488 load(e,nullptr,this);
490 // Inject "default" app ID if not explicit.
491 if (!getString("applicationId").first)
492 setProperty("applicationId", "default");
494 // Load any AccessControl provider.
497 pair<bool,bool> unicodeAware = getBool("unicodeAware");
498 m_unicodeAware = (unicodeAware.first && unicodeAware.second);
500 // Loop over the HostRegex elements.
501 const DOMElement* host = XMLHelper::getFirstChildElement(e,HostRegex);
502 for (int i=1; host; ++i, host=XMLHelper::getNextSiblingElement(host,HostRegex)) {
503 const XMLCh* n=host->getAttributeNS(nullptr,regex);
505 log.warn("Skipping HostRegex element (%d) with empty regex attribute",i);
509 auto_ptr<Override> o(new Override(m_unicodeAware, host, log, this));
511 const XMLCh* flag=host->getAttributeNS(nullptr,ignoreCase);
513 auto_ptr<RegularExpression> re(
514 new RegularExpression(n, (flag && (*flag==chLatin_f || *flag==chDigit_0)) ? &chNull : ignoreOption)
516 m_regexps.push_back(make_pair(re.release(), o.release()));
518 catch (XMLException& ex) {
519 auto_ptr_char tmp(ex.getMessage());
520 log.error("caught exception while parsing HostRegex regular expression (%d): %s", i, tmp.get());
523 log.debug("Added <HostRegex> mapping for %s", m_regexps.back().second->getString("regex").second);
526 // Loop over the Host elements.
527 host = XMLHelper::getFirstChildElement(e,Host);
528 for (int i=1; host; ++i, host=XMLHelper::getNextSiblingElement(host,Host)) {
529 const XMLCh* n=host->getAttributeNS(nullptr,name);
531 log.warn("Skipping Host element (%d) with empty name attribute",i);
535 Override* o=new Override(m_unicodeAware, host, log, this);
536 pair<bool,const char*> name=o->getString("name");
537 pair<bool,const char*> scheme=o->getString("scheme");
538 pair<bool,const char*> port=o->getString("port");
540 char* dup=strdup(name.second);
541 for (char* pch=dup; *pch; pch++)
543 auto_ptr<char> dupwrap(dup);
545 if (!scheme.first && port.first) {
546 // No scheme, but a port, so assume http.
547 scheme = pair<bool,const char*>(true,"http");
549 else if (scheme.first && !port.first) {
550 // Scheme, no port, so default it.
551 // XXX Use getservbyname instead?
553 if (!strcmp(scheme.second,"http"))
555 else if (!strcmp(scheme.second,"https"))
557 else if (!strcmp(scheme.second,"ftp"))
559 else if (!strcmp(scheme.second,"ldap"))
561 else if (!strcmp(scheme.second,"ldaps"))
566 string url(scheme.second);
567 url=url + "://" + dup;
569 // Is this the default port?
570 if ((!strcmp(scheme.second,"http") && !strcmp(port.second,"80")) ||
571 (!strcmp(scheme.second,"https") && !strcmp(port.second,"443")) ||
572 (!strcmp(scheme.second,"ftp") && !strcmp(port.second,"21")) ||
573 (!strcmp(scheme.second,"ldap") && !strcmp(port.second,"389")) ||
574 (!strcmp(scheme.second,"ldaps") && !strcmp(port.second,"636"))) {
575 // First store a port-less version.
576 if (m_map.count(url) || m_extras.count(url)) {
577 log.warn("Skipping duplicate Host element (%s)",url.c_str());
582 log.debug("Added <Host> mapping for %s",url.c_str());
584 // Now append the port. We use the extras vector, to avoid double freeing the object later.
585 url=url + ':' + port.second;
587 log.debug("Added <Host> mapping for %s",url.c_str());
590 url=url + ':' + port.second;
591 if (m_map.count(url) || m_extras.count(url)) {
592 log.warn("Skipping duplicate Host element (%s)",url.c_str());
597 log.debug("Added <Host> mapping for %s",url.c_str());
601 // No scheme or port, so we enter dual hosts on http:80 and https:443
602 string url("http://");
604 if (m_map.count(url) || m_extras.count(url)) {
605 log.warn("Skipping duplicate Host element (%s)",url.c_str());
610 log.debug("Added <Host> mapping for %s",url.c_str());
613 if (m_map.count(url) || m_extras.count(url)) {
614 log.warn("Skipping duplicate Host element (%s)",url.c_str());
618 log.debug("Added <Host> mapping for %s",url.c_str());
622 if (m_map.count(url) || m_extras.count(url)) {
623 log.warn("Skipping duplicate Host element (%s)",url.c_str());
627 log.debug("Added <Host> mapping for %s",url.c_str());
630 if (m_map.count(url) || m_extras.count(url)) {
631 log.warn("Skipping duplicate Host element (%s)",url.c_str());
635 log.debug("Added <Host> mapping for %s",url.c_str());
640 const Override* XMLRequestMapperImpl::findOverride(const char* vhost, const HTTPRequest& request) const
642 const Override* o=nullptr;
643 map<string,Override*>::const_iterator i=m_map.find(vhost);
647 i=m_extras.find(vhost);
648 if (i!=m_extras.end())
651 for (vector< pair<RegularExpression*,Override*> >::const_iterator re = m_regexps.begin(); !o && re != m_regexps.end(); ++re) {
652 if (re->first->matches(vhost))
658 return o ? o->locate(request) : this;
661 pair<bool,DOMElement*> XMLRequestMapper::background_load()
663 // Load from source using base class.
664 pair<bool,DOMElement*> raw = ReloadableXMLFile::load();
666 // If we own it, wrap it.
667 XercesJanitor<DOMDocument> docjanitor(raw.first ? raw.second->getOwnerDocument() : nullptr);
669 XMLRequestMapperImpl* impl = new XMLRequestMapperImpl(raw.second, m_log);
671 // If we held the document, transfer it to the impl. If we didn't, it's a no-op.
672 impl->setDocument(docjanitor.release());
674 // Perform the swap inside a lock.
677 SharedLock locker(m_lock, false);
681 return make_pair(false,(DOMElement*)nullptr);
684 RequestMapper::Settings XMLRequestMapper::getSettings(const HTTPRequest& request) const
688 vhost << request.getScheme() << "://" << request.getHostname() << ':' << request.getPort();
689 const Override* o=m_impl->findOverride(vhost.str().c_str(), request);
690 return Settings(o,o->getAC());
692 catch (XMLException& ex) {
693 auto_ptr_char tmp(ex.getMessage());
694 m_log.error("caught exception while locating content settings: %s", tmp.get());
695 throw ConfigurationException("XML-based RequestMapper failed to retrieve content settings.");