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

org.opensaml.saml.metadata.resolver.impl.HTTPMetadataResolver 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.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Timer;

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

import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.primitive.DeprecationSupport;
import net.shibboleth.utilities.java.support.primitive.DeprecationSupport.ObjectType;
import net.shibboleth.utilities.java.support.resolver.ResolverException;

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.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
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.apache.http.util.EntityUtils;
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;

/**
 * A metadata provider that pulls metadata using an HTTP GET. Metadata is cached until one of these criteria is met:
 * 
    *
  • The smallest cacheDuration within the metadata is exceeded
  • *
  • The earliest validUntil time within the metadata is exceeded
  • *
  • The maximum cache duration is exceeded
  • *
* * Metadata is filtered prior to determining the cache expiration data. This allows a filter to remove XMLObjects that * may effect the cache duration but for which the user of this provider does not care about. * * It is the responsibility of the caller to re-initialize, via {@link #initialize()}, if any properties of this * provider are changed. */ public class HTTPMetadataResolver extends AbstractReloadingMetadataResolver { /** Class logger. */ private final Logger log = LoggerFactory.getLogger(HTTPMetadataResolver.class); /** HTTP Client used to pull the metadata. */ private HttpClient httpClient; /** URL to the Metadata. */ private URI metadataURI; /** The ETag provided when the currently cached metadata was fetched. */ private String cachedMetadataETag; /** The Last-Modified information provided when the currently cached metadata was fetched. */ private String cachedMetadataLastModified; /** HttpClient credentials provider. * @deprecated use {@link #httpClientSecurityParameters}. * */ @Nullable private BasicCredentialsProvider 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 HTTP client used to pull in remote metadata * @param metadataURL URL to the remove remote metadata * * @throws ResolverException thrown if the HTTP client is null or the metadata URL provided is invalid */ public HTTPMetadataResolver(final HttpClient client, final String metadataURL) throws ResolverException { this(null, client, metadataURL); } /** * Constructor. * * @param backgroundTaskTimer timer used to schedule background metadata refresh tasks * @param client HTTP client used to pull in remote metadata * @param metadataURL URL to the remove remote metadata * * @throws ResolverException thrown if the HTTP client is null or the metadata URL provided is invalid */ public HTTPMetadataResolver(final Timer backgroundTaskTimer, final HttpClient client, final String metadataURL) throws ResolverException { super(backgroundTaskTimer); if (client == null) { throw new ResolverException("HTTP client may not be null"); } httpClient = client; try { metadataURI = new URI(metadataURL); } catch (final URISyntaxException e) { throw new ResolverException("Illegal URL syntax", e); } } /** * Gets the URL to fetch the metadata. * * @return the URL to fetch the metadata */ public String getMetadataURI() { return metadataURI.toASCIIString(); } /** * 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; } /** * Sets the username and password used to access the metadata URL. To disable BASIC authentication pass null for the * credentials instance. * * An {@link AuthScope} will be generated based off the metadata URI's hostname and port. * * @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); } /** * Sets the username and password used to access the metadata URL. To disable BASIC authentication pass null for the * credentials instance. * *

* If the authScope is null, an {@link AuthScope} will be generated based off the metadata URI's * hostname and port. *

* * @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(metadataURI.getHost(), metadataURI.getPort()); } final BasicCredentialsProvider provider = new BasicCredentialsProvider(); provider.setCredentials(authScope, credentials); credentialsProvider = provider; } else { log.debug("Either username or password were null, disabling basic auth"); 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; } /** {@inheritDoc} */ @Override protected void doDestroy() { httpClient = null; tlsTrustEngine = null; credentialsProvider = null; httpClientSecurityParameters = null; metadataURI = null; cachedMetadataETag = null; cachedMetadataLastModified = null; super.doDestroy(); } /** {@inheritDoc} */ @Override protected String getMetadataIdentifier() { return metadataURI.toString(); } /** * Gets the metadata document from the remote server. * * @return the metadata from remote server, or null if the metadata document has not changed since the last * retrieval * * @throws ResolverException thrown if there is a problem retrieving the metadata from the remote server */ @Override protected byte[] fetchMetadata() throws ResolverException { final HttpGet httpGet = buildHttpGet(); final HttpClientContext context = buildHttpClientContext(httpGet); HttpResponse response = null; try { log.debug("{} Attempting to fetch metadata document from '{}'", getLogPrefix(), metadataURI); response = httpClient.execute(httpGet, context); HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, metadataURI.getScheme()); final int httpStatusCode = response.getStatusLine().getStatusCode(); if (httpStatusCode == HttpStatus.SC_NOT_MODIFIED) { log.debug("{} Metadata document from '{}' has not changed since last retrieval", getLogPrefix(), getMetadataURI()); return null; } if (httpStatusCode != HttpStatus.SC_OK) { final String errMsg = "Non-ok status code " + httpStatusCode + " returned from remote metadata source " + metadataURI; log.error("{} " + errMsg, getLogPrefix()); throw new ResolverException(errMsg); } processConditionalRetrievalHeaders(response); final byte[] rawMetadata = getMetadataBytesFromResponse(response); log.debug("{} Successfully fetched {} bytes of metadata from {}", getLogPrefix(), rawMetadata.length, getMetadataURI()); return rawMetadata; } catch (final IOException e) { final String errMsg = "Error retrieving metadata from " + metadataURI; log.error("{} " + errMsg, getLogPrefix(), e); throw new ResolverException(errMsg, e); } finally { try { if (response != null && response instanceof CloseableHttpResponse) { ((CloseableHttpResponse) response).close(); } } catch (final IOException e) { log.error("{} Error closing HTTP response from {}", metadataURI, getLogPrefix(), e); } } } /** * Check that trust engine evaluation of the server TLS credential was actually performed. * * @param context the current HTTP context instance in use * @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) throws SSLPeerUnverifiedException { DeprecationSupport.warnOnce(ObjectType.METHOD, getClass().getName()+ ".checkTLSCredentialTrusted", null, "HttpClientSecuritySupport.checkTLSCredentialEvaluated(..)"); HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, metadataURI.getScheme()); } /** * Builds the {@link HttpGet} instance used to fetch the metadata. The returned method advertises support for GZIP * and deflate compression, enables conditional GETs if the cached metadata came with either an ETag or * Last-Modified information, and sets up basic authentication if such is configured. * * @return the constructed HttpGet instance */ protected HttpGet buildHttpGet() { final HttpGet getMethod = new HttpGet(getMetadataURI()); if (cachedMetadataETag != null) { getMethod.setHeader("If-None-Match", cachedMetadataETag); } if (cachedMetadataLastModified != null) { getMethod.setHeader("If-Modified-Since", cachedMetadataLastModified); } return getMethod; } /** * 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; } /** * Records the ETag and Last-Modified headers, from the response, if they are present. * * @param response GetMethod containing a valid HTTP response */ protected void processConditionalRetrievalHeaders(final HttpResponse response) { Header httpHeader = response.getFirstHeader("ETag"); if (httpHeader != null) { cachedMetadataETag = httpHeader.getValue(); } httpHeader = response.getFirstHeader("Last-Modified"); if (httpHeader != null) { cachedMetadataLastModified = httpHeader.getValue(); } } /** * Extracts the raw metadata bytes from the response taking in to account possible deflate and GZip compression. * * @param response GetMethod containing a valid HTTP response * * @return the raw metadata bytes * * @throws ResolverException thrown if there is a problem getting the raw metadata bytes from the response */ protected byte[] getMetadataBytesFromResponse(final HttpResponse response) throws ResolverException { log.debug("{} Attempting to extract metadata from response to request for metadata from '{}'", getLogPrefix(), getMetadataURI()); try { final InputStream ins = response.getEntity().getContent(); return inputstreamToByteArray(ins); } catch (final IOException e) { log.error("{} Unable to read response", getLogPrefix(), e); throw new ResolverException("Unable to read response", e); } finally { // Make sure entity has been completely consumed. EntityUtils.consumeQuietly(response.getEntity()); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy