SSPCPP-616 - clean up concatenated string literals
[shibboleth/cpp-sp.git] / shibsp / metadata / DynamicMetadataProvider.cpp
1 /**
2  * Licensed to the University Corporation for Advanced Internet
3  * Development, Inc. (UCAID) under one or more contributor license
4  * agreements. See the NOTICE file distributed with this work for
5  * additional information regarding copyright ownership.
6  *
7  * UCAID licenses this file to you under the Apache License,
8  * Version 2.0 (the "License"); you may not use this file except
9  * in compliance with the License. You may obtain a copy of the
10  * License at
11  *
12  * http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
17  * either express or implied. See the License for the specific
18  * language governing permissions and limitations under the License.
19  */
20
21 /**
22  * DynamicMetadataProvider.cpp
23  *
24  * Advanced implementation of a dynamic caching MetadataProvider.
25  */
26
27 #include "internal.h"
28 #include "exceptions.h"
29 #include "Application.h"
30 #include "ServiceProvider.h"
31 #include "metadata/MetadataProviderCriteria.h"
32
33 #include <boost/algorithm/string.hpp>
34 #include <xercesc/framework/Wrapper4InputSource.hpp>
35 #include <xercesc/util/XMLUniDefs.hpp>
36 #include <xercesc/util/regx/RegularExpression.hpp>
37 #include <xsec/framework/XSECDefs.hpp>
38
39 #include <saml/version.h>
40 #include <saml/binding/SAMLArtifact.h>
41 #include <saml/saml2/metadata/Metadata.h>
42 #include <saml/saml2/metadata/DynamicMetadataProvider.h>
43
44 #include <xmltooling/logging.h>
45 #include <xmltooling/XMLToolingConfig.h>
46 #include <xmltooling/security/Credential.h>
47 #include <xmltooling/security/CredentialCriteria.h>
48 #include <xmltooling/security/CredentialResolver.h>
49 #include <xmltooling/security/X509TrustEngine.h>
50 #include <xmltooling/soap/HTTPSOAPTransport.h>
51 #include <xmltooling/util/NDC.h>
52 #include <xmltooling/util/ParserPool.h>
53 #include <xmltooling/util/URLEncoder.h>
54 #include <xmltooling/util/XMLHelper.h>
55
56 using namespace shibsp;
57 using namespace opensaml;
58 using namespace xmltooling::logging;
59 using namespace xmltooling;
60 using namespace std;
61
62 namespace shibsp {
63     class SHIBSP_DLLLOCAL DynamicMetadataProvider : public saml2md::DynamicMetadataProvider
64     {
65     public:
66         DynamicMetadataProvider(const xercesc::DOMElement* e=nullptr);
67
68         virtual ~DynamicMetadataProvider() {}
69
70     protected:
71         saml2md::EntityDescriptor* resolve(const saml2md::MetadataProvider::Criteria& criteria) const;
72
73     private:
74         bool m_verifyHost,m_ignoreTransport,m_encoded;
75         string m_subst, m_match, m_regex;
76         boost::scoped_ptr<X509TrustEngine> m_trust;
77         boost::scoped_ptr<CredentialResolver> m_dummyCR;
78     };
79
80     saml2md::MetadataProvider* SHIBSP_DLLLOCAL DynamicMetadataProviderFactory(const DOMElement* const & e)
81     {
82         return new DynamicMetadataProvider(e);
83     }
84
85     static const XMLCh encoded[] =          UNICODE_LITERAL_7(e,n,c,o,d,e,d);
86     static const XMLCh ignoreTransport[] =  UNICODE_LITERAL_15(i,g,n,o,r,e,T,r,a,n,s,p,o,r,t);
87     static const XMLCh match[] =            UNICODE_LITERAL_5(m,a,t,c,h);
88     static const XMLCh Regex[] =            UNICODE_LITERAL_5(R,e,g,e,x);
89     static const XMLCh Subst[] =            UNICODE_LITERAL_5(S,u,b,s,t);
90     static const XMLCh _TrustEngine[] =     UNICODE_LITERAL_11(T,r,u,s,t,E,n,g,i,n,e);
91     static const XMLCh _type[] =            UNICODE_LITERAL_4(t,y,p,e);
92     static const XMLCh verifyHost[] =       UNICODE_LITERAL_10(v,e,r,i,f,y,H,o,s,t);
93 };
94
95 DynamicMetadataProvider::DynamicMetadataProvider(const DOMElement* e)
96     : saml2md::DynamicMetadataProvider(e),
97         m_verifyHost(XMLHelper::getAttrBool(e, true, verifyHost)),
98         m_ignoreTransport(XMLHelper::getAttrBool(e, false, ignoreTransport)),
99         m_encoded(true), m_trust(nullptr)
100 {
101     const DOMElement* child = XMLHelper::getFirstChildElement(e, Subst);
102     if (child && child->hasChildNodes()) {
103         auto_ptr_char s(child->getFirstChild()->getNodeValue());
104         if (s.get() && *s.get()) {
105             m_subst = s.get();
106             m_encoded = XMLHelper::getAttrBool(child, true, encoded);
107         }
108     }
109
110     if (m_subst.empty()) {
111         child = XMLHelper::getFirstChildElement(e, Regex);
112         if (child && child->hasChildNodes() && child->hasAttributeNS(nullptr, match)) {
113             m_match = XMLHelper::getAttrString(child, nullptr, match);
114             auto_ptr_char repl(child->getFirstChild()->getNodeValue());
115             if (repl.get() && *repl.get())
116                 m_regex = repl.get();
117         }
118     }
119
120     if (!m_ignoreTransport) {
121         child = XMLHelper::getFirstChildElement(e, _TrustEngine);
122         string t = XMLHelper::getAttrString(child, nullptr, _type);
123         if (!t.empty()) {
124             TrustEngine* trust = XMLToolingConfig::getConfig().TrustEngineManager.newPlugin(t.c_str(), child);
125             if (!dynamic_cast<X509TrustEngine*>(trust)) {
126                 delete trust;
127                 throw ConfigurationException("DynamicMetadataProvider requires an X509TrustEngine plugin.");
128             }
129             m_trust.reset(dynamic_cast<X509TrustEngine*>(trust));
130             m_dummyCR.reset(XMLToolingConfig::getConfig().CredentialResolverManager.newPlugin(DUMMY_CREDENTIAL_RESOLVER, nullptr));
131         }
132
133         if (!m_trust.get() || !m_dummyCR.get())
134             throw ConfigurationException("DynamicMetadataProvider requires an X509TrustEngine plugin unless ignoreTransport is true.");
135     }
136 }
137
138 saml2md::EntityDescriptor* DynamicMetadataProvider::resolve(const saml2md::MetadataProvider::Criteria& criteria) const
139 {
140 #ifdef _DEBUG
141     xmltooling::NDC("resolve");
142 #endif
143     Category& log=Category::getInstance(SHIBSP_LOGCAT ".MetadataProvider.Dynamic");
144
145     string name;
146     if (criteria.entityID_ascii) {
147         name = criteria.entityID_ascii;
148     }
149     else if (criteria.entityID_unicode) {
150         auto_ptr_char temp(criteria.entityID_unicode);
151         name = temp.get();
152     }
153     else if (criteria.artifact) {
154         if (m_subst.empty() && (m_regex.empty() || m_match.empty()))
155             throw saml2md::MetadataException("Unable to resolve metadata dynamically from an artifact.");
156         name = "{sha1}" + criteria.artifact->getSource();
157     }
158
159     // Possibly transform the input into a different URL to use.
160     if (!m_subst.empty()) {
161         string name2 = boost::replace_first_copy(m_subst, "$entityID",
162             m_encoded ? XMLToolingConfig::getConfig().getURLEncoder()->encode(name.c_str()) : name);
163         log.info("transformed location from (%s) to (%s)", name.c_str(), name2.c_str());
164         name = name2;
165     }
166     else if (!m_match.empty() && !m_regex.empty()) {
167         try {
168             RegularExpression exp(m_match.c_str());
169             XMLCh* temp = exp.replace(name.c_str(), m_regex.c_str());
170             if (temp) {
171                 auto_ptr_char narrow(temp);
172                 XMLString::release(&temp);
173
174                 // For some reason it returns the match string if it doesn't match the expression.
175                 if (name != narrow.get()) {
176                     log.info("transformed location from (%s) to (%s)", name.c_str(), narrow.get());
177                     name = narrow.get();
178                 }
179             }
180         }
181         catch (XMLException& ex) {
182             auto_ptr_char msg(ex.getMessage());
183             log.error("caught error applying regular expression: %s", msg.get());
184         }
185     }
186
187     // Establish networking properties based on calling application.
188     const MetadataProviderCriteria* mpc = dynamic_cast<const MetadataProviderCriteria*>(&criteria);
189     if (!mpc)
190         throw saml2md::MetadataException("Dynamic MetadataProvider requires Shibboleth-aware lookup criteria, check calling code.");
191     const PropertySet* relyingParty;
192     if (criteria.artifact)
193         relyingParty = mpc->application.getRelyingParty((XMLCh*)nullptr);
194     else if (criteria.entityID_unicode)
195         relyingParty = mpc->application.getRelyingParty(criteria.entityID_unicode);
196     else {
197         auto_ptr_XMLCh temp2(name.c_str());
198         relyingParty = mpc->application.getRelyingParty(temp2.get());
199     }
200
201     // Prepare a transport object addressed appropriately.
202     SOAPTransport::Address addr(relyingParty->getString("entityID").second, name.c_str(), name.c_str());
203     const char* pch = strchr(addr.m_endpoint,':');
204     if (!pch)
205         throw IOException("location was not a URL.");
206     string scheme(addr.m_endpoint, pch-addr.m_endpoint);
207     boost::scoped_ptr<SOAPTransport> transport;
208     try {
209         transport.reset(XMLToolingConfig::getConfig().SOAPTransportManager.newPlugin(scheme.c_str(), addr));
210     }
211     catch (exception& ex) {
212         log.error("exception while building transport object to resolve URL: %s", ex.what());
213         throw IOException("Unable to resolve entityID with a known transport protocol.");
214     }
215
216     // Apply properties as directed.
217     transport->setVerifyHost(m_verifyHost);
218     if (m_trust.get() && m_dummyCR.get() && !transport->setTrustEngine(m_trust.get(), m_dummyCR.get()))
219         throw IOException("Unable to install X509TrustEngine into transport object.");
220
221     Locker credlocker(nullptr, false);
222     CredentialResolver* credResolver = nullptr;
223     pair<bool,const char*> authType=relyingParty->getString("authType");
224     if (!authType.first || !strcmp(authType.second,"TLS")) {
225         credResolver = mpc->application.getCredentialResolver();
226         if (credResolver)
227             credlocker.assign(credResolver);
228         if (credResolver) {
229             CredentialCriteria cc;
230             cc.setUsage(Credential::TLS_CREDENTIAL);
231             authType = relyingParty->getString("keyName");
232             if (authType.first)
233                 cc.getKeyNames().insert(authType.second);
234             const Credential* cred = credResolver->resolve(&cc);
235             cc.getKeyNames().clear();
236             if (cred) {
237                 if (!transport->setCredential(cred))
238                     log.error("failed to load Credential into metadata resolver");
239             }
240             else {
241                 log.error("no TLS credential supplied");
242             }
243         }
244         else {
245             log.error("no CredentialResolver available for TLS");
246         }
247     }
248     else {
249         SOAPTransport::transport_auth_t type=SOAPTransport::transport_auth_none;
250         pair<bool,const char*> username=relyingParty->getString("authUsername");
251         pair<bool,const char*> password=relyingParty->getString("authPassword");
252         if (!username.first || !password.first)
253             log.error("transport authType (%s) specified but authUsername or authPassword was missing", authType.second);
254         else if (!strcmp(authType.second,"basic"))
255             type = SOAPTransport::transport_auth_basic;
256         else if (!strcmp(authType.second,"digest"))
257             type = SOAPTransport::transport_auth_digest;
258         else if (!strcmp(authType.second,"ntlm"))
259             type = SOAPTransport::transport_auth_ntlm;
260         else if (!strcmp(authType.second,"gss"))
261             type = SOAPTransport::transport_auth_gss;
262         else if (strcmp(authType.second,"none"))
263             log.error("unknown authType (%s) specified for RelyingParty", authType.second);
264         if (type > SOAPTransport::transport_auth_none) {
265             if (transport->setAuth(type,username.second,password.second))
266                 log.debug("configured for transport authentication (method=%s, username=%s)", authType.second, username.second);
267             else
268                 log.error("failed to configure transport authentication (method=%s)", authType.second);
269         }
270     }
271
272     pair<bool,unsigned int> timeout = relyingParty->getUnsignedInt("connectTimeout");
273     transport->setConnectTimeout(timeout.first ? timeout.second : 10);
274     timeout = relyingParty->getUnsignedInt("timeout");
275     transport->setTimeout(timeout.first ? timeout.second : 20);
276     mpc->application.getServiceProvider().setTransportOptions(*transport);
277
278     HTTPSOAPTransport* http = dynamic_cast<HTTPSOAPTransport*>(transport.get());
279     if (http) {
280         pair<bool,bool> flag = relyingParty->getBool("chunkedEncoding");
281         http->useChunkedEncoding(flag.first && flag.second);
282         http->setRequestHeader("Xerces-C", XERCES_FULLVERSIONDOT);
283         http->setRequestHeader("XML-Security-C", XSEC_FULLVERSIONDOT);
284         http->setRequestHeader("OpenSAML-C", gOpenSAMLDotVersionStr);
285         http->setRequestHeader(PACKAGE_NAME, PACKAGE_VERSION);
286     }
287
288     try {
289         // Use a nullptr stream to trigger a body-less "GET" operation.
290         transport->send();
291         istream& msg = transport->receive();
292
293         DOMDocument* doc=nullptr;
294         StreamInputSource src(msg, "DynamicMetadataProvider");
295         Wrapper4InputSource dsrc(&src,false);
296         if (m_validate)
297             doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
298         else
299             doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
300
301         // Wrap the document for now.
302         XercesJanitor<DOMDocument> docjanitor(doc);
303
304         // Check root element.
305         if (!doc->getDocumentElement() || !XMLHelper::isNodeNamed(doc->getDocumentElement(),
306                 samlconstants::SAML20MD_NS, saml2md::EntityDescriptor::LOCAL_NAME)) {
307             throw saml2md::MetadataException("Root of metadata instance was not an EntityDescriptor");
308         }
309
310         // Unmarshall objects, binding the document.
311         auto_ptr<XMLObject> xmlObject(XMLObjectBuilder::buildOneFromElement(doc->getDocumentElement(), true));
312         docjanitor.release();
313
314         // Make sure it's metadata.
315         saml2md::EntityDescriptor* entity = dynamic_cast<saml2md::EntityDescriptor*>(xmlObject.get());
316         if (!entity) {
317             throw saml2md::MetadataException(
318                 "Root of metadata instance not recognized: $1", params(1,xmlObject->getElementQName().toString().c_str())
319                 );
320         }
321         xmlObject.release();
322         return entity;
323     }
324     catch (XMLException& e) {
325         auto_ptr_char msg(e.getMessage());
326         log.error("Xerces error while resolving location (%s): %s", name.c_str(), msg.get());
327         throw saml2md::MetadataException(msg.get());
328     }
329 }