View Javadoc

1   /*
2    * Copyright [2006] [University Corporation for Advanced Internet Development, Inc.]
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  package org.opensaml.saml2.metadata.provider;
18  
19  import java.io.IOException;
20  import java.net.URI;
21  import java.net.URISyntaxException;
22  
23  import org.apache.commons.httpclient.HttpClient;
24  import org.apache.commons.httpclient.UsernamePasswordCredentials;
25  import org.apache.commons.httpclient.auth.AuthScope;
26  import org.apache.commons.httpclient.methods.GetMethod;
27  import org.apache.commons.httpclient.params.HttpClientParams;
28  import org.apache.commons.httpclient.protocol.Protocol;
29  import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
30  import org.joda.time.DateTime;
31  import org.opensaml.saml2.common.SAML2Helper;
32  import org.opensaml.xml.XMLObject;
33  import org.opensaml.xml.io.UnmarshallingException;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  /**
38   * A metadata provider that pulls metadata using an HTTP GET. Metadata is cached until one of these criteria is met:
39   * <ul>
40   * <li>The smallest cacheDuration within the metadata is exceeded</li>
41   * <li>The earliest validUntil time within the metadata is exceeded</li>
42   * <li>The maximum cache duration is exceeded</li>
43   * </ul>
44   * 
45   * Metadata is filtered prior to determining the cache expiration data. This allows a filter to remove XMLObjects that
46   * may effect the cache duration but for which the user of this provider does not care about.
47   * 
48   * It is the responsibility of the caller to re-initialize, via {@link #initialize()}, if any properties of this
49   * provider are changed.
50   */
51  public class HTTPMetadataProvider extends AbstractObservableMetadataProvider {
52  
53      /** Cached, filtered, unmarshalled metadata. */
54      private XMLObject cachedMetadata;
55  
56      /** Class logger. */
57      private final Logger log = LoggerFactory.getLogger(HTTPMetadataProvider.class);
58  
59      /** URL to the Metadata. */
60      private URI metadataURI;
61  
62      /** Whether cached metadata should be discarded if it expires and can't be refreshed. */
63      private boolean maintainExpiredMetadata;
64  
65      /** HTTP Client used to pull the metadata. */
66      private HttpClient httpClient;
67  
68      /** URL scope that requires authentication. */
69      private AuthScope authScope;
70  
71      /** Maximum amount of time to keep metadata cached. */
72      private int maxCacheDuration;
73  
74      /** When the cached metadata becomes stale. */
75      private DateTime mdExpirationTime;
76  
77      /**
78       * Constructor.
79       * 
80       * @param metadataURL the URL to fetch the metadata
81       * @param requestTimeout the time, in milliseconds, to wait for the metadata server to respond
82       * 
83       * @throws MetadataProviderException thrown if the URL is not a valid URL or the metadata can not be retrieved from
84       *             the URL
85       */
86      public HTTPMetadataProvider(String metadataURL, int requestTimeout) throws MetadataProviderException {
87          super();
88          try {
89              metadataURI = new URI(metadataURL);
90              maintainExpiredMetadata = true;
91  
92              HttpClientParams clientParams = new HttpClientParams();
93              clientParams.setSoTimeout(requestTimeout);
94              httpClient = new HttpClient(clientParams);
95              httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(requestTimeout); 
96              authScope = new AuthScope(metadataURI.getHost(), metadataURI.getPort());
97  
98              // 24 hours
99              maxCacheDuration = 60 * 60 * 24;
100         } catch (URISyntaxException e) {
101             throw new MetadataProviderException("Illegal URL syntax", e);
102         }
103     }
104 
105     /**
106      * Initializes the provider and prepares it for use.
107      * 
108      * @throws MetadataProviderException thrown if there is a problem fetching, parsing, or processing the metadata
109      */
110     public void initialize() throws MetadataProviderException {
111         refreshMetadata();
112     }
113 
114     /**
115      * Gets the URL to fetch the metadata.
116      * 
117      * @return the URL to fetch the metadata
118      */
119     public String getMetadataURI() {
120         return metadataURI.toASCIIString();
121     }
122 
123     /**
124      * Gets whether cached metadata should be discarded if it expires and can not be refreshed.
125      * 
126      * @return whether cached metadata should be discarded if it expires and can not be refreshed
127      */
128     public boolean maintainExpiredMetadata() {
129         return maintainExpiredMetadata;
130     }
131 
132     /**
133      * Sets whether cached metadata should be discarded if it expires and can not be refreshed.
134      * 
135      * @param maintain whether cached metadata should be discarded if it expires and can not be refreshed
136      */
137     public void setMaintainExpiredMetadata(boolean maintain) {
138         maintainExpiredMetadata = maintain;
139     }
140 
141     /**
142      * Sets the username and password used to access the metadata URL. To disable BASIC authentication set the username
143      * and password to null;
144      * 
145      * @param username the username
146      * @param password the password
147      */
148     public void setBasicCredentials(String username, String password) {
149         if (username == null && password == null) {
150             httpClient.getState().setCredentials(null, null);
151         } else {
152             UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
153             httpClient.getState().setCredentials(authScope, credentials);
154         }
155     }
156 
157     /**
158      * Gets the length of time in milliseconds to wait for the server to respond.
159      * 
160      * @return length of time in milliseconds to wait for the server to respond
161      */
162     public int getRequestTimeout() {
163         return httpClient.getParams().getSoTimeout();
164     }
165 
166     /**
167      * Sets the socket factory used to create sockets to the HTTP server.
168      * 
169      * @see <a href="http://jakarta.apache.org/commons/httpclient/sslguide.html">HTTPClient SSL guide</a>
170      * 
171      * @param newSocketFactory the socket factory used to produce sockets used to connect to the server
172      */
173     public void setSocketFactory(ProtocolSocketFactory newSocketFactory) {
174         log.debug("Using the custom socket factory {} to connect to the HTTP server", newSocketFactory.getClass()
175                 .getName());
176         Protocol protocol = new Protocol(metadataURI.getScheme(), newSocketFactory, metadataURI.getPort());
177         httpClient.getHostConfiguration().setHost(metadataURI.getHost(), metadataURI.getPort(), protocol);
178     }
179 
180     /**
181      * Gets the maximum amount of time, in seconds, metadata will be cached for.
182      * 
183      * @return the maximum amount of time metadata will be cached for
184      */
185     public int getMaxCacheDuration() {
186         return maxCacheDuration;
187     }
188 
189     /**
190      * Sets the maximum amount of time, in seconds, metadata will be cached for.
191      * 
192      * @param newDuration the maximum amount of time metadata will be cached for
193      */
194     public void setMaxCacheDuration(int newDuration) {
195         maxCacheDuration = newDuration;
196     }
197 
198     /** {@inheritDoc} */
199     public XMLObject getMetadata() throws MetadataProviderException {
200         if (mdExpirationTime.isBeforeNow()) {
201             log.debug("Cached metadata is stale, refreshing");
202             refreshMetadata();
203         }
204 
205         return cachedMetadata;
206     }
207 
208     /**
209      * Caches the metadata.
210      * 
211      * @param metadata metadata to cache
212      */
213     protected void cacheMetadata(XMLObject metadata) {
214         cachedMetadata = metadata;
215     }
216 
217     /**
218      * Refreshes the metadata cache. Metadata is fetched from the URL through an HTTP get, unmarshalled, and then
219      * filtered. This method also clears out the entity ID to entity descriptor cache.
220      * 
221      * @throws MetadataProviderException thrown if the metadata can not be read, unmarshalled, and filtered
222      */
223     protected synchronized void refreshMetadata() throws MetadataProviderException {
224         if (mdExpirationTime != null && !mdExpirationTime.isBeforeNow()) {
225             // In case other requests stacked up behind the synchronize lock
226             return;
227         }
228 
229         log.debug("Refreshing cache of metadata from URL {}, max cache duration set to {} seconds", metadataURI,
230                 maxCacheDuration);
231         try {
232             XMLObject metadata = fetchMetadata();
233 
234             log.debug("Calculating expiration time");
235             DateTime now = new DateTime();
236             mdExpirationTime = SAML2Helper.getEarliestExpiration(metadata, now.plus(maxCacheDuration * 1000), now);
237             log.debug("Metadata cache expires on " + mdExpirationTime);
238 
239             if (mdExpirationTime != null && !maintainExpiredMetadata() && mdExpirationTime.isBeforeNow()) {
240                 cachedMetadata = null;
241             } else {
242                 filterMetadata(metadata);
243                 releaseMetadataDOM(metadata);
244                 cachedMetadata = metadata;
245             }
246 
247             emitChangeEvent();
248         } catch (IOException e) {
249             String errorMsg = "Unable to read metadata from server";
250             log.error(errorMsg, e);
251             throw new MetadataProviderException(errorMsg, e);
252         } catch (UnmarshallingException e) {
253             String errorMsg = "Unable to unmarshall metadata";
254             log.error(errorMsg, e);
255             throw new MetadataProviderException(errorMsg, e);
256         } catch (FilterException e) {
257             String errorMsg = "Unable to filter metadata";
258             log.error(errorMsg, e);
259             throw new MetadataProviderException(errorMsg, e);
260         }
261     }
262 
263     /**
264      * Fetches the metadata from the remote server and unmarshalls it.
265      * 
266      * @return the unmarshalled metadata
267      * 
268      * @throws IOException thrown if the metadata can not be fetched from the remote server
269      * @throws UnmarshallingException thrown if the metadata can not be unmarshalled
270      */
271     protected XMLObject fetchMetadata() throws IOException, UnmarshallingException {
272         log.debug("Fetching metadata document from remote server");
273         GetMethod getMethod = new GetMethod(getMetadataURI());
274         if (httpClient.getState().getCredentials(authScope) != null) {
275             log.debug("Using BASIC authentication when retrieving metadata");
276             getMethod.setDoAuthentication(true);
277         }
278         httpClient.executeMethod(getMethod);
279 
280         if (log.isTraceEnabled()) {
281             log.trace("Retrieved the following metadata document\n{}", getMethod.getResponseBodyAsString());
282         }
283         XMLObject metadata = unmarshallMetadata(getMethod.getResponseBodyAsStream());
284 
285         log.debug("Unmarshalled metadata from remote server");
286         return metadata;
287 
288     }
289 }