View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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  package org.kuali.rice.ksb.messaging.serviceconnectors;
17  
18  import org.apache.http.client.HttpRequestRetryHandler;
19  import org.apache.http.client.config.CookieSpecs;
20  import org.apache.http.client.config.RequestConfig;
21  import org.apache.http.config.ConnectionConfig;
22  import org.apache.http.config.Registry;
23  import org.apache.http.config.RegistryBuilder;
24  import org.apache.http.config.SocketConfig;
25  import org.apache.http.conn.HttpClientConnectionManager;
26  import org.apache.http.conn.socket.ConnectionSocketFactory;
27  import org.apache.http.conn.socket.PlainConnectionSocketFactory;
28  import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
29  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
30  import org.apache.http.conn.ssl.SSLContextBuilder;
31  import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
32  import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
33  import org.apache.http.impl.client.HttpClientBuilder;
34  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
35  import org.kuali.rice.core.api.config.property.ConfigContext;
36  import org.kuali.rice.core.api.exception.RiceRuntimeException;
37  import org.kuali.rice.ksb.util.KSBConstants;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  import org.springframework.beans.factory.InitializingBean;
41  
42  import java.nio.charset.Charset;
43  import java.security.KeyManagementException;
44  import java.security.KeyStoreException;
45  import java.security.NoSuchAlgorithmException;
46  import java.util.Arrays;
47  import java.util.HashSet;
48  import java.util.Map;
49  import java.util.Set;
50  
51  import static org.kuali.rice.ksb.messaging.serviceconnectors.HttpClientParams.*;
52  
53  /**
54   * Configures HttpClientBuilder instances for use by the HttpInvokerConnector.
55   *
56   * <p>This class adapts the configuration mechanism which was used with Commons HttpClient, which used a number of
57   * specific Rice config params (see {@link HttpClientParams}) to work with  the HttpComponents HttpClient.  The
58   * configuration doesn't all map across nicely, so coverage is not perfect.</p>
59   *
60   * <p>If the configuration parameters here are not sufficient, this implementation is designed to be extended.</p>
61   *
62   * @author Kuali Rice Team (rice.collab@kuali.org)
63   */
64  public class DefaultHttpClientConfigurer implements HttpClientConfigurer, InitializingBean {
65  
66      static final Logger LOG = LoggerFactory.getLogger(DefaultHttpClientConfigurer.class);
67  
68      private static final String RETRY_SOCKET_EXCEPTION_PROPERTY = "ksb.thinClient.retrySocketException";
69      private static final int DEFAULT_SOCKET_TIMEOUT = 2 * 60 * 1000; // two minutes in milliseconds
70  
71      /**
72       * Default maximum total connections per client
73       */
74      private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 20;
75  
76      // list of config params starting with "http." to ignore when looking for unsupported params
77      private static final Set<String> unsupportedParamsWhitelist =
78              new HashSet<String>(Arrays.asList("http.port", "http.service.url"));
79  
80      /**
81       * Customizes the configuration of the httpClientBuilder.
82       *
83       * <p>Internally, this uses several helper methods to assist with configuring:
84       * <ul>
85       *     <li>Calls {@link #buildConnectionManager()} and sets the resulting {@link HttpClientConnectionManager} (if
86       *     non-null) into the httpClientBuilder.</li>
87       *     <li>Calls {@link #buildRequestConfig()} and sets the resulting {@link RequestConfig} (if non-null) into the
88       *     httpClientBuilder.</li>
89       *     <li>Calls {@link #buildRetryHandler()} and sets the resulting {@link HttpRequestRetryHandler} (if non-null)
90       *     into the httpClientBuilder.</li>
91       * </ul>
92       * </p>
93       *
94       * @param httpClientBuilder the httpClientBuilder being configured
95       */
96      @Override
97      public void customizeHttpClient(HttpClientBuilder httpClientBuilder) {
98  
99          HttpClientConnectionManager connectionManager = buildConnectionManager();
100         if (connectionManager != null) {
101             httpClientBuilder.setConnectionManager(connectionManager);
102         }
103 
104         RequestConfig requestConfig = buildRequestConfig();
105         if (requestConfig != null) {
106             httpClientBuilder.setDefaultRequestConfig(requestConfig);
107         }
108 
109         HttpRequestRetryHandler retryHandler = buildRetryHandler();
110         if (retryHandler != null) {
111             httpClientBuilder.setRetryHandler(retryHandler);
112         }
113     }
114 
115     /**
116      * Builds the HttpClientConnectionManager.
117      *
118      * <p>Note that this calls {@link #buildSslConnectionSocketFactory()} and registers the resulting {@link SSLConnectionSocketFactory}
119      * (if non-null) with its socket factory registry.</p>
120      *
121      * @return the HttpClientConnectionManager
122      */
123     protected HttpClientConnectionManager buildConnectionManager() {
124         PoolingHttpClientConnectionManager poolingConnectionManager = null;
125 
126         SSLConnectionSocketFactory sslConnectionSocketFactory = buildSslConnectionSocketFactory();
127         if (sslConnectionSocketFactory != null) {
128             Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder
129                     .<ConnectionSocketFactory> create().register("https", sslConnectionSocketFactory)
130                     .register("http", new PlainConnectionSocketFactory())
131                     .build();
132             poolingConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
133         } else {
134             poolingConnectionManager = new PoolingHttpClientConnectionManager();
135         }
136 
137         // Configure the connection manager
138         poolingConnectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS.getValueOrDefault(DEFAULT_MAX_TOTAL_CONNECTIONS));
139 
140         // By default we'll set the max connections per route (essentially that means per host for us) to the max total
141         poolingConnectionManager.setDefaultMaxPerRoute(MAX_TOTAL_CONNECTIONS.getValueOrDefault(DEFAULT_MAX_TOTAL_CONNECTIONS));
142 
143 
144         SocketConfig.Builder socketConfigBuilder = SocketConfig.custom();
145         socketConfigBuilder.setSoTimeout(SO_TIMEOUT.getValueOrDefault(DEFAULT_SOCKET_TIMEOUT));
146 
147         Integer soLinger = SO_LINGER.getValue();
148         if (soLinger != null) {
149             socketConfigBuilder.setSoLinger(soLinger);
150         }
151 
152         Boolean isTcpNoDelay = TCP_NODELAY.getValue();
153         if (isTcpNoDelay != null) {
154             socketConfigBuilder.setTcpNoDelay(isTcpNoDelay);
155         }
156 
157         poolingConnectionManager.setDefaultSocketConfig(socketConfigBuilder.build());
158 
159         ConnectionConfig.Builder connectionConfigBuilder = ConnectionConfig.custom();
160 
161         Integer sendBuffer = SO_SNDBUF.getValue();
162         Integer receiveBuffer = SO_RCVBUF.getValue();
163 
164         // if either send or recieve buffer size is set, we'll set the buffer size to whichever is greater
165         if (sendBuffer != null || receiveBuffer != null) {
166             Integer bufferSize = -1;
167             if (sendBuffer != null) {
168                 bufferSize = sendBuffer;
169             }
170 
171             if (receiveBuffer != null && receiveBuffer > bufferSize) {
172                 bufferSize = receiveBuffer;
173             }
174 
175             connectionConfigBuilder.setBufferSize(bufferSize);
176         }
177 
178         String contentCharset = HTTP_CONTENT_CHARSET.getValue();
179         if (contentCharset != null) {
180             connectionConfigBuilder.setCharset(Charset.forName(contentCharset));
181         }
182 
183         poolingConnectionManager.setDefaultConnectionConfig(connectionConfigBuilder.build());
184 
185         return poolingConnectionManager;
186     }
187 
188     /**
189      * Builds the retry handler if {@link #RETRY_SOCKET_EXCEPTION_PROPERTY} is true in the project's configuration.
190      *
191      * @return the HttpRequestRetryHandler or null depending on configuration
192      */
193     protected HttpRequestRetryHandler buildRetryHandler() {
194         // If configured to do so, allow the client to retry once
195         if (ConfigContext.getCurrentContextConfig().getBooleanProperty(RETRY_SOCKET_EXCEPTION_PROPERTY, false)) {
196             return new DefaultHttpRequestRetryHandler(1, true);
197         }
198 
199         return null;
200     }
201 
202     /**
203      * Configures and builds the RequestConfig for the HttpClient.
204      *
205      * @return the RequestConfig
206      */
207     protected RequestConfig buildRequestConfig() {
208         RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
209 
210         // was using "rfc2109" here, but apparently RFC-2956 is standard now.
211         requestConfigBuilder.setCookieSpec(COOKIE_POLICY.getValueOrDefault(CookieSpecs.STANDARD));
212 
213         Integer connectionRequestTimeout = CONNECTION_MANAGER_TIMEOUT.getValue();
214         if (connectionRequestTimeout != null) {
215             requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeout);
216         }
217 
218         Integer connectionTimeout = CONNECTION_TIMEOUT.getValue();
219         if (connectionTimeout != null) {
220             requestConfigBuilder.setConnectTimeout(connectionTimeout);
221         }
222 
223         Boolean isStaleConnectionCheckEnabled = STALE_CONNECTION_CHECK.getValue();
224         if (isStaleConnectionCheckEnabled != null) {
225             requestConfigBuilder.setStaleConnectionCheckEnabled(isStaleConnectionCheckEnabled);
226         }
227 
228         requestConfigBuilder.setSocketTimeout(SO_TIMEOUT.getValueOrDefault(DEFAULT_SOCKET_TIMEOUT));
229 
230         Boolean isUseExpectContinue = USE_EXPECT_CONTINUE.getValue();
231         if (isUseExpectContinue != null) {
232             requestConfigBuilder.setExpectContinueEnabled(isUseExpectContinue);
233         }
234 
235         Integer maxRedirects = MAX_REDIRECTS.getValue();
236         if (maxRedirects != null) {
237             requestConfigBuilder.setMaxRedirects(maxRedirects);
238         }
239 
240         Boolean isCircularRedirectsAllowed = ALLOW_CIRCULAR_REDIRECTS.getValue();
241         if (isCircularRedirectsAllowed != null) {
242             requestConfigBuilder.setCircularRedirectsAllowed(isCircularRedirectsAllowed);
243         }
244 
245         Boolean isRejectRelativeRedirects = REJECT_RELATIVE_REDIRECT.getValue();
246         if (isRejectRelativeRedirects != null) {
247             // negating the parameter value here to align with httpcomponents:
248             requestConfigBuilder.setRelativeRedirectsAllowed(!isRejectRelativeRedirects);
249         }
250 
251         return requestConfigBuilder.build();
252     }
253 
254     /**
255      * Builds the {@link SSLConnectionSocketFactory} used in the connection manager's socket factory registry.
256      *
257      * <p>Note that if {@link org.kuali.rice.ksb.util.KSBConstants.Config#KSB_ALLOW_SELF_SIGNED_SSL} is set to true
258      * in the project configuration, this connection factory will be configured to accept self signed certs even if
259      * the hostname doesn't match.</p>
260      *
261      * @return the SSLConnectionSocketFactory
262      */
263     protected SSLConnectionSocketFactory buildSslConnectionSocketFactory() {
264         SSLContextBuilder builder = new SSLContextBuilder();
265 
266         if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KSBConstants.Config.KSB_ALLOW_SELF_SIGNED_SSL)) {
267             try {
268                 // allow self signed certs
269                 builder.loadTrustMaterial(null, new TrustSelfSignedStrategy());
270             } catch (NoSuchAlgorithmException e) {
271                 throw new RiceRuntimeException(e);
272             } catch (KeyStoreException e) {
273                 throw new RiceRuntimeException(e);
274             }
275         }
276 
277         SSLConnectionSocketFactory sslsf = null;
278 
279         try {
280             if (ConfigContext.getCurrentContextConfig().getBooleanProperty(KSBConstants.Config.KSB_ALLOW_SELF_SIGNED_SSL)) {
281                 // allow certs that don't match the hostname
282                 sslsf = new SSLConnectionSocketFactory(builder.build(), new AllowAllHostnameVerifier());
283             } else {
284                 sslsf = new SSLConnectionSocketFactory(builder.build());
285             }
286         } catch (NoSuchAlgorithmException e) {
287             throw new RiceRuntimeException(e);
288         } catch (KeyManagementException e) {
289             throw new RiceRuntimeException(e);
290         }
291 
292         return sslsf;
293     }
294 
295     /**
296      * Exercises the configuration to make it fail fast if there is a problem.
297      *
298      * @throws Exception
299      */
300     @Override
301     public void afterPropertiesSet() throws Exception {
302         customizeHttpClient(HttpClientBuilder.create());
303 
304         // Warn about any params that look like they are for the old Commons HttpClient config
305         Map<String, String> httpParams = ConfigContext.getCurrentContextConfig().getPropertiesWithPrefix("http.", false);
306 
307         for (Map.Entry<String, String> paramEntry : httpParams.entrySet()) {
308             if (!isParamNameSupported(paramEntry.getKey()) && !unsupportedParamsWhitelist.contains(paramEntry)) {
309                 LOG.warn("Ignoring unsupported config param \"" + paramEntry.getKey() + "\" with value \"" + paramEntry.getValue() + "\"");
310             }
311         }
312     }
313 
314     /**
315      * Checks all the enum elements in HttpClientParams to see if the given param name is supported.
316      *
317      * @param paramName
318      * @return true if HttpClientParams contains an element with that param name
319      */
320     private boolean isParamNameSupported(String paramName) {
321         for (HttpClientParams param : HttpClientParams.values()) {
322             if (param.getParamName().equals(paramName)) {
323                 return true;
324             }
325         }
326 
327         return false;
328     }
329 }