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