org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.java

Source

/*
 * 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.saml2.metadata.provider;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Timer;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
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:
 * <ul>
 * <li>The smallest cacheDuration within the metadata is exceeded</li>
 * <li>The earliest validUntil time within the metadata is exceeded</li>
 * <li>The maximum cache duration is exceeded</li>
 * </ul>
 * 
 * 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 HTTPMetadataProvider extends AbstractReloadingMetadataProvider {

    /** Class logger. */
    private final Logger log = LoggerFactory.getLogger(HTTPMetadataProvider.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;

    /** URL scope that requires authentication. */
    private AuthScope authScope;

    /**
     * Constructor.
     * 
     * @param metadataURL the URL to fetch the metadata
     * @param requestTimeout the time, in milliseconds, to wait for the metadata server to respond
     * 
     * @throws MetadataProviderException thrown if the URL is not a valid URL or the metadata can not be retrieved from
     *             the URL
     */
    @Deprecated
    public HTTPMetadataProvider(String metadataURL, int requestTimeout) throws MetadataProviderException {
        super();
        try {
            metadataURI = new URI(metadataURL);
        } catch (URISyntaxException e) {
            throw new MetadataProviderException("Illegal URL syntax", e);
        }

        HttpClientParams clientParams = new HttpClientParams();
        clientParams.setSoTimeout(requestTimeout);
        httpClient = new HttpClient(clientParams);
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(requestTimeout);
        authScope = new AuthScope(metadataURI.getHost(), metadataURI.getPort());

    }

    /**
     * Constructor.
     * 
     * @param client HTTP client used to pull in remote metadata
     * @param backgroundTaskTimer timer used to schedule background metadata refresh tasks
     * @param metadataURL URL to the remove remote metadata
     * 
     * @throws MetadataProviderException thrown if the HTTP client is null or the metadata URL provided is invalid
     */
    public HTTPMetadataProvider(Timer backgroundTaskTimer, HttpClient client, String metadataURL)
            throws MetadataProviderException {
        super(backgroundTaskTimer);

        if (client == null) {
            throw new MetadataProviderException("HTTP client may not be null");
        }
        httpClient = client;

        try {
            metadataURI = new URI(metadataURL);
        } catch (URISyntaxException e) {
            throw new MetadataProviderException("Illegal URL syntax", e);
        }

        authScope = new AuthScope(metadataURI.getHost(), metadataURI.getPort());
    }

    /**
     * Gets the URL to fetch the metadata.
     * 
     * @return the URL to fetch the metadata
     */
    public String getMetadataURI() {
        return metadataURI.toASCIIString();
    }

    /**
     * Sets the username and password used to access the metadata URL. To disable BASIC authentication set the username
     * and password to null;
     * 
     * @param username the username
     * @param password the password
     */
    public void setBasicCredentials(String username, String password) {
        if (username == null && password == null) {
            httpClient.getState().setCredentials(null, null);
        } else {
            UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
            httpClient.getState().setCredentials(authScope, credentials);
        }
    }

    /**
     * Gets the length of time in milliseconds to wait for the server to respond.
     * 
     * @return length of time in milliseconds to wait for the server to respond
     */
    public int getRequestTimeout() {
        return httpClient.getParams().getSoTimeout();
    }

    /**
     * Sets the socket factory used to create sockets to the HTTP server.
     * 
     * @see <a href="http://jakarta.apache.org/commons/httpclient/sslguide.html">HTTPClient SSL guide</a>
     * 
     * @param newSocketFactory the socket factory used to produce sockets used to connect to the server
     * 
     * @deprecated set this information on HTTP client used by provider
     */
    public void setSocketFactory(ProtocolSocketFactory newSocketFactory) {
        log.debug("Using the custom socket factory {} to connect to the HTTP server",
                newSocketFactory.getClass().getName());
        Protocol protocol = new Protocol(metadataURI.getScheme(), newSocketFactory, metadataURI.getPort());
        httpClient.getHostConfiguration().setHost(metadataURI.getHost(), metadataURI.getPort(), protocol);
    }

    /**
     * Gets the maximum amount of time, in seconds, metadata will be cached for.
     * 
     * @return maximum amount of time, in seconds, metadata will be cached for
     * 
     * @deprecated use {@link #getMaxRefreshDelay()} instead
     */
    public int getMaxCacheDuration() {
        return (int) getMaxRefreshDelay();
    }

    /**
     * Sets the maximum amount of time, in seconds, metadata will be cached for.
     * 
     * @param newDuration maximum amount of time, in seconds, metadata will be cached for
     * 
     * @deprecated use {@link #setMaxRefreshDelay(long)} instead
     */
    public void setMaxCacheDuration(int newDuration) {
        setMaxRefreshDelay(newDuration * 1000);
    }

    /**
     * Gets whether cached metadata should be discarded if it expires and can not be refreshed.
     * 
     * @return whether cached metadata should be discarded if it expires and can not be refreshed.
     * 
     * @deprecated use {@link #requireValidMetadata()} instead
     */
    public boolean maintainExpiredMetadata() {
        return !requireValidMetadata();
    }

    /**
     * Sets whether cached metadata should be discarded if it expires and can not be refreshed.
     * 
     * @param maintain whether cached metadata should be discarded if it expires and can not be refreshed.
     * 
     * @deprecated use {@link #setRequireValidMetadata(boolean)} instead
     */
    public void setMaintainExpiredMetadata(boolean maintain) {
        setRequireValidMetadata(!maintain);
    }

    /** {@inheritDoc} */
    public synchronized void destroy() {
        httpClient = null;
        metadataURI = null;
        cachedMetadataETag = null;
        cachedMetadataLastModified = null;
        authScope = null;

        super.destroy();
    }

    /** {@inheritDoc} */
    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 MetadataProviderException thrown if there is a problem retrieving the metadata from the remote server
     */
    protected byte[] fetchMetadata() throws MetadataProviderException {
        GetMethod getMethod = buildGetMethod();

        try {
            log.debug("Attempting to fetch metadata document from '{}'", metadataURI);
            httpClient.executeMethod(getMethod);
            int httpStatus = getMethod.getStatusCode();

            if (httpStatus == HttpStatus.SC_NOT_MODIFIED) {
                log.debug("Metadata document from '{}' has not changed since last retrieval", getMetadataURI());
                return null;
            }

            if (getMethod.getStatusCode() != HttpStatus.SC_OK) {
                String errMsg = "Non-ok status code " + getMethod.getStatusCode()
                        + " returned from remote metadata source " + metadataURI;
                log.error(errMsg);
                throw new MetadataProviderException(errMsg);
            }

            processConditionalRetrievalHeaders(getMethod);

            byte[] rawMetadata = getMetadataBytesFromResponse(getMethod);
            log.debug("Successfully fetched {}bytes of metadata from {}", rawMetadata.length, getMetadataURI());

            return rawMetadata;
        } catch (IOException e) {
            String errMsg = "Error retrieving metadata from " + metadataURI;
            log.error(errMsg, e);
            throw new MetadataProviderException(errMsg, e);
        } finally {
            getMethod.releaseConnection();
        }
    }

    /**
     * Builds the HTTP GET method 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 GET method
     */
    protected GetMethod buildGetMethod() {
        GetMethod getMethod = new GetMethod(getMetadataURI());
        getMethod.addRequestHeader("Connection", "close");

        getMethod.setRequestHeader("Accept-Encoding", "gzip,deflate");
        if (cachedMetadataETag != null) {
            getMethod.setRequestHeader("If-None-Match", cachedMetadataETag);
        }
        if (cachedMetadataLastModified != null) {
            getMethod.setRequestHeader("If-Modified-Since", cachedMetadataLastModified);
        }

        if (httpClient.getState().getCredentials(authScope) != null) {
            log.debug("Using BASIC authentication when retrieving metadata from '{}", metadataURI);
            getMethod.setDoAuthentication(true);
        }

        return getMethod;
    }

    /**
     * Records the ETag and Last-Modified headers, from the response, if they are present.
     * 
     * @param getMethod GetMethod containing a valid HTTP response
     */
    protected void processConditionalRetrievalHeaders(GetMethod getMethod) {
        Header httpHeader = getMethod.getResponseHeader("ETag");
        if (httpHeader != null) {
            cachedMetadataETag = httpHeader.getValue();
        }

        httpHeader = getMethod.getResponseHeader("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 getMethod GetMethod containing a valid HTTP response
     * 
     * @return the raw metadata bytes
     * 
     * @throws MetadataProviderException thrown if there is a problem getting the raw metadata bytes from the response
     */
    protected byte[] getMetadataBytesFromResponse(GetMethod getMethod) throws MetadataProviderException {
        log.debug("Attempting to extract metadata from response to request for metadata from '{}'",
                getMetadataURI());
        try {
            InputStream ins = getMethod.getResponseBodyAsStream();

            Header httpHeader = getMethod.getResponseHeader("Content-Encoding");
            if (httpHeader != null) {
                String contentEncoding = httpHeader.getValue();
                if ("deflate".equalsIgnoreCase(contentEncoding)) {
                    log.debug("Metadata document from '{}' was deflate compressed, decompressing it", metadataURI);
                    ins = new InflaterInputStream(ins);
                }

                if ("gzip".equalsIgnoreCase(contentEncoding)) {
                    log.debug("Metadata document from '{}' was GZip compressed, decompressing it", metadataURI);
                    ins = new GZIPInputStream(ins);
                }
            }

            return inputstreamToByteArray(ins);
        } catch (IOException e) {
            log.error("Unable to read response", e);
            throw new MetadataProviderException("Unable to read response", e);
        }
    }
}