All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensaml.saml.metadata.resolver.impl.AbstractDynamicHTTPMetadataResolver Maven / Gradle / Ivy

/*
 * Licensed to the University Corporation for Advanced Internet Development,
 * Inc. (UCAID) under one or more contributor license agreements.  See the
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID licenses this file to You under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.opensaml.saml.metadata.resolver.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.Timer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.net.ssl.SSLPeerUnverifiedException;

import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSource;
import org.opensaml.security.httpclient.HttpClientSecurityConstants;
import org.opensaml.security.httpclient.HttpClientSecurityParameters;
import org.opensaml.security.httpclient.HttpClientSecuritySupport;
import org.opensaml.security.trust.TrustEngine;
import org.opensaml.security.x509.X509Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.io.ByteStreams;
import com.google.common.net.MediaType;

import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
import net.shibboleth.utilities.java.support.collection.LazySet;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.net.MediaTypeSupport;
import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;

/**
 * Abstract subclass for dynamic metadata resolvers that implement metadata resolution based on HTTP requests.
 */
public abstract class AbstractDynamicHTTPMetadataResolver extends AbstractDynamicMetadataResolver {
    
    /** Default list of supported content MIME types. */
    public static final String[] DEFAULT_CONTENT_TYPES = 
            new String[] {"application/samlmetadata+xml", "application/xml", "text/xml"};
    
    /** MDC attribute representing the current request URI. Will be available during the execution of the 
     * configured {@link ResponseHandler}. */
    public static final String MDC_ATTRIB_CURRENT_REQUEST_URI = 
            AbstractDynamicHTTPMetadataResolver.class.getName() + ".currentRequestURI";
    
    /** Class logger. */
    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicHTTPMetadataResolver.class);
    
    /** HTTP Client used to pull the metadata. */
    @Nonnull private HttpClient httpClient;
    
    /** List of supported MIME types for use in Accept request header and validation of 
     * response Content-Type header.*/
    @NonnullAfterInit private List supportedContentTypes;
    
    /** Generated Accept request header value. */
    @NonnullAfterInit private String supportedContentTypesValue;
    
    /**Supported {@link MediaType} instances, constructed from the {@link #supportedContentTypes} list. */
    @NonnullAfterInit private Set supportedMediaTypes;
    
    /** HttpClient ResponseHandler instance to use. */
    @Nonnull private ResponseHandler responseHandler;
    
    /** HttpClient credentials provider. 
     * @deprecated use {@link #httpClientSecurityParameters}.
     * */
    @Nullable private CredentialsProvider credentialsProvider;
    
    /** Optional trust engine used in evaluating server TLS credentials.
     * @deprecated use {@link #httpClientSecurityParameters}.
     *  */
    @Nullable private TrustEngine tlsTrustEngine;
    
    /** Optional HttpClient security parameters.*/
    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
    
    /**
     * Constructor.
     *
     * @param client the instance of {@link HttpClient} used to fetch remote metadata
     */
    public AbstractDynamicHTTPMetadataResolver(@Nonnull final HttpClient client) {
        this(null, client);
    }
    
    /**
     * Constructor.
     *
     * @param backgroundTaskTimer the {@link Timer} instance used to run resolver background managment tasks
     * @param client the instance of {@link HttpClient} used to fetch remote metadata
     */
    public AbstractDynamicHTTPMetadataResolver(@Nullable final Timer backgroundTaskTimer, 
            @Nonnull final HttpClient client) {
        super(backgroundTaskTimer);
        
        httpClient = Constraint.isNotNull(client, "HttpClient may not be null");
        
        // The default handler
        responseHandler = new BasicMetadataResponseHandler();
    }
    
    /**
     * Sets the optional trust engine used in evaluating server TLS credentials.
     * 
     * 

* See TLS socket factory requirements documented for * {@link #setHttpClientSecurityParameters(HttpClientSecurityParameters)}. *

* * @param engine the trust engine instance to use * * @deprecated use {@link #setHttpClientSecurityParameters(HttpClientSecurityParameters)} */ public void setTLSTrustEngine(@Nullable final TrustEngine engine) { DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName() + ".setTLSTrustEngine", null, "setHttpClientSecurityParameters(HttpClientSecurityParameters)"); tlsTrustEngine = engine; } /** * Set an instance of {@link CredentialsProvider} used for authentication by the HttpClient instance. * * @param provider the credentials provider * * @deprecated use {@link #setHttpClientSecurityParameters(HttpClientSecurityParameters)} */ public void setCredentialsProvider(@Nullable final CredentialsProvider provider) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName() + ".setCredentialsProvider", null, "setHttpClientSecurityParameters(HttpClientSecurityParameters)"); credentialsProvider = provider; } /** * A convenience method to set a (single) username and password used to access metadata. * To disable BASIC authentication pass null for the credentials instance. * *

* An {@link AuthScope} will be generated which specifies any host, port, scheme and realm. *

* *

To specify multiple usernames and passwords for multiple host, port, scheme, and realm combinations, instead * provide an instance of {@link CredentialsProvider} via {@link #setCredentialsProvider(CredentialsProvider)}.

* * @param credentials the username and password credentials * * @deprecated use {@link #setHttpClientSecurityParameters(HttpClientSecurityParameters)} */ public void setBasicCredentials(@Nullable final UsernamePasswordCredentials credentials) { DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName() + ".setBasicCredentials", null, "setHttpClientSecurityParameters(HttpClientSecurityParameters)"); setBasicCredentialsWithScope(credentials, null); } /** * A convenience method to set a (single) username and password used to access metadata. * To disable BASIC authentication pass null for the credentials instance. * *

* If the authScope is null, an {@link AuthScope} will be generated which specifies * any host, port, scheme and realm. *

* *

To specify multiple usernames and passwords for multiple host, port, scheme, and realm combinations, instead * provide an instance of {@link CredentialsProvider} via {@link #setCredentialsProvider(CredentialsProvider)}.

* * @param credentials the username and password credentials * @param scope the HTTP client auth scope with which to scope the credentials, may be null * * @deprecated use {@link #setHttpClientSecurityParameters(HttpClientSecurityParameters)} */ public void setBasicCredentialsWithScope(@Nullable final UsernamePasswordCredentials credentials, @Nullable final AuthScope scope) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName() + ".setBasicCredentialsWithScope", null, "setHttpClientSecurityParameters(HttpClientSecurityParameters)"); if (credentials != null) { AuthScope authScope = scope; if (authScope == null) { authScope = new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT); } final BasicCredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials(authScope, credentials); credentialsProvider = provider; } else { log.debug("{} Either username or password were null, disabling basic auth", getLogPrefix()); credentialsProvider = null; } } /** * Get the instance of {@link HttpClientSecurityParameters} which provides various parameters to influence * the security behavior of the HttpClient instance. * * @return the parameters instance, or null */ @Nullable protected HttpClientSecurityParameters getHttpClientSecurityParameters() { return httpClientSecurityParameters; } /** * Set an instance of {@link HttpClientSecurityParameters} which provides various parameters to influence * the security behavior of the HttpClient instance. * *

* For all TLS-related parameters, must be used in conjunction with an HttpClient instance * which is configured with either a: *

    *
  • * a {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory} *
  • *
  • * {@link org.opensaml.security.httpclient.impl.SecurityEnhancedTLSSocketFactory} which wraps * an instance of {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory}, with * the latter likely configured in a "no trust" configuration. This variant is required if either a * trust engine or a client TLS credential is to be used. *
  • * For convenience methods for building a * {@link net.shibboleth.utilities.java.support.httpclient.TLSSocketFactory}, * see {@link net.shibboleth.utilities.java.support.httpclient.HttpClientSupport}. *
* If the appropriate TLS socket factory is not configured and a trust engine is specified, * then this will result in no TLS trust evaluation being performed and a * {@link ResolverException} will ultimately be thrown. *

* @param params the security parameters */ public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); httpClientSecurityParameters = params; } /** * Get the list of supported MIME {@link MediaType} instances used in validation of * the response Content-Type header. * *

* Is generated at init time from {@link #getSupportedContentTypes()}. *

* * @return the supported content types */ @NonnullAfterInit @NotLive @Unmodifiable protected Set getSupportedMediaTypes() { return supportedMediaTypes; } /** * Get the list of supported MIME types for use in Accept request header and validation of * response Content-Type header. * * @return the supported content types */ @NonnullAfterInit @NotLive @Unmodifiable public List getSupportedContentTypes() { return supportedContentTypes; } /** * Set the list of supported MIME types for use in Accept request header and validation of * response Content-Type header. Values will be effectively lower-cased at runtime. * * @param types the new supported content types to set */ public void setSupportedContentTypes(@Nullable final List types) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); if (types == null) { supportedContentTypes = Collections.emptyList(); } else { supportedContentTypes = new ArrayList<>(Collections2.transform( StringSupport.normalizeStringCollection(types), new Function() { @Override @Nullable public String apply(@Nullable final String input) { return input == null ? null : input.toLowerCase(); } } )); } } /** {@inheritDoc} */ @Override protected void initMetadataResolver() throws ComponentInitializationException { super.initMetadataResolver(); if (getSupportedContentTypes() == null) { setSupportedContentTypes(Arrays.asList(DEFAULT_CONTENT_TYPES)); } if (! getSupportedContentTypes().isEmpty()) { supportedContentTypesValue = StringSupport.listToStringValue(getSupportedContentTypes(), ", "); supportedMediaTypes = new LazySet<>(); for (final String contentType : getSupportedContentTypes()) { supportedMediaTypes.add(MediaType.parse(contentType)); } } else { supportedMediaTypes = Collections.emptySet(); } log.debug("{} Supported content types are: {}", getLogPrefix(), getSupportedContentTypes()); } /** {@inheritDoc} */ @Override protected void doDestroy() { httpClient = null; credentialsProvider = null; tlsTrustEngine = null; httpClientSecurityParameters = null; supportedContentTypes = null; supportedContentTypesValue = null; supportedMediaTypes = null; super.doDestroy(); } /** {@inheritDoc} */ @Override @Nullable protected XMLObject fetchFromOriginSource(@Nonnull final CriteriaSet criteria) throws IOException { final HttpUriRequest request = buildHttpRequest(criteria); if (request == null) { log.debug("{} Could not build request based on input criteria, unable to query", getLogPrefix()); return null; } final HttpClientContext context = buildHttpClientContext(request); try { MDC.put(MDC_ATTRIB_CURRENT_REQUEST_URI, request.getURI().toString()); final XMLObject result = httpClient.execute(request, responseHandler, context); HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme()); return result; } finally { MDC.remove(MDC_ATTRIB_CURRENT_REQUEST_URI); } } /** * Check that trust engine evaluation of the server TLS credential was actually performed. * * @param context the current HTTP context instance in use * @param request the HTTP URI request * @throws SSLPeerUnverifiedException thrown if the TLS credential was not actually evaluated by the trust engine * * @deprecated use {@link HttpClientSecuritySupport#checkTLSCredentialEvaluated(HttpClientContext, String)} */ @Deprecated protected void checkTLSCredentialTrusted(final HttpClientContext context, final HttpUriRequest request) throws SSLPeerUnverifiedException { DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName() + ".checkTLSCredentialTrusted", null, "HttpClientSecuritySupport.checkTLSCredentialEvaluated(HttpClientContext, String)"); HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme()); } /** * Build an appropriate instance of {@link HttpUriRequest} based on the input criteria set. * * @param criteria the input criteria set * @return the newly constructed request, or null if it can not be built from the supplied criteria */ @Nullable protected HttpUriRequest buildHttpRequest(@Nonnull final CriteriaSet criteria) { final String url = buildRequestURL(criteria); log.debug("{} Built request URL of: {}", getLogPrefix(), url); if (url == null) { log.debug("{} Could not construct request URL from input criteria, unable to query", getLogPrefix()); return null; } final HttpGet getMethod = new HttpGet(url); if (!Strings.isNullOrEmpty(supportedContentTypesValue)) { getMethod.addHeader("Accept", supportedContentTypesValue); } // TODO other headers ? return getMethod; } /** * Build the request URL based on the input criteria set. * * @param criteria the input criteria set * @return the request URL, or null if it can not be built based on the supplied criteria */ @Nullable protected abstract String buildRequestURL(@Nonnull final CriteriaSet criteria); /** * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request. * * @return a new instance of {@link HttpClientContext} * * @deprecated use {@link #buildHttpClientContext(HttpUriRequest)} */ protected HttpClientContext buildHttpClientContext() { //TODO when we remove this deprecated method, change called method to @Nonnull for request DeprecationSupport.warn(ObjectType.METHOD, getClass().getName() + ".buildHttpClientContext()", null, null); return buildHttpClientContext(null); } /** * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request. * * @param request the current HTTP request * * @return a new instance of {@link HttpClientContext} */ protected HttpClientContext buildHttpClientContext(@Nullable final HttpUriRequest request) { // TODO Really request should be @Nonnull, change when we remove deprecated buildHttpClientContext() final HttpClientContext context = HttpClientContext.create(); HttpClientSecuritySupport.marshalSecurityParameters(context, httpClientSecurityParameters, true); // If these legacy values are present, let them override the above params instance values unconditionally if (credentialsProvider != null) { context.setCredentialsProvider(credentialsProvider); } if (tlsTrustEngine != null) { context.setAttribute(HttpClientSecurityConstants.CONTEXT_KEY_TRUST_ENGINE, tlsTrustEngine); } if (request != null) { HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(context, request); } return context; } /** * Basic HttpClient response handler for processing metadata fetch requests. */ public class BasicMetadataResponseHandler implements ResponseHandler { /** {@inheritDoc} */ @Override public XMLObject handleResponse(@Nonnull final HttpResponse response) throws IOException { final int httpStatusCode = response.getStatusLine().getStatusCode(); final String currentRequestURI = MDC.get(MDC_ATTRIB_CURRENT_REQUEST_URI); // TODO should we be seeing/doing this? Probably not if we don't do conditional GET. // But we will if we do pre-emptive refreshing of metadata in background thread. if (httpStatusCode == HttpStatus.SC_NOT_MODIFIED) { log.debug("{} Metadata document from '{}' has not changed since last retrieval", getLogPrefix(), currentRequestURI); return null; } if (httpStatusCode != HttpStatus.SC_OK) { log.warn("{} Non-ok status code '{}' returned from remote metadata source: {}", getLogPrefix(), httpStatusCode, currentRequestURI); return null; } try { validateHttpResponse(response); } catch (final ResolverException e) { log.error("{} Problem validating dynamic metadata HTTP response", getLogPrefix(), e); return null; } try { final InputStream ins = response.getEntity().getContent(); final byte[] source = ByteStreams.toByteArray(ins); try (ByteArrayInputStream bais = new ByteArrayInputStream(source)) { final XMLObject xmlObject = unmarshallMetadata(bais); xmlObject.getObjectMetadata().put(new XMLObjectSource(source)); return xmlObject; } } catch (final IOException | UnmarshallingException e) { log.error("{} Error unmarshalling HTTP response stream", getLogPrefix(), e); return null; } } /** * Validate the received HTTP response instance, such as checking for supported content types. * * @param response the received response * @throws ResolverException if the response was not valid, or if there is a fatal error validating the response */ protected void validateHttpResponse(@Nonnull final HttpResponse response) throws ResolverException { if (!getSupportedMediaTypes().isEmpty()) { String contentTypeValue = null; final Header contentType = response.getEntity().getContentType(); if (contentType != null && contentType.getValue() != null) { contentTypeValue = StringSupport.trimOrNull(contentType.getValue()); } log.debug("{} Saw raw Content-Type from response header '{}'", getLogPrefix(), contentTypeValue); if (!MediaTypeSupport.validateContentType(contentTypeValue, getSupportedMediaTypes(), true, false)) { throw new ResolverException("HTTP response specified an unsupported Content-Type MIME type: " + contentTypeValue); } } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy