001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.ksb.messaging.serviceconnectors;
017
018import org.apache.http.client.HttpRequestRetryHandler;
019import org.apache.http.client.config.CookieSpecs;
020import org.apache.http.client.config.RequestConfig;
021import org.apache.http.config.ConnectionConfig;
022import org.apache.http.config.Registry;
023import org.apache.http.config.RegistryBuilder;
024import org.apache.http.config.SocketConfig;
025import org.apache.http.conn.HttpClientConnectionManager;
026import org.apache.http.conn.socket.ConnectionSocketFactory;
027import org.apache.http.conn.socket.PlainConnectionSocketFactory;
028import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
029import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
030import org.apache.http.conn.ssl.SSLContextBuilder;
031import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
032import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
033import org.apache.http.impl.client.HttpClientBuilder;
034import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
035import org.kuali.rice.core.api.config.property.ConfigContext;
036import org.kuali.rice.core.api.exception.RiceRuntimeException;
037import org.kuali.rice.ksb.util.KSBConstants;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040import org.springframework.beans.factory.InitializingBean;
041
042import java.nio.charset.Charset;
043import java.security.KeyManagementException;
044import java.security.KeyStoreException;
045import java.security.NoSuchAlgorithmException;
046import java.util.Arrays;
047import java.util.HashSet;
048import java.util.Map;
049import java.util.Set;
050
051import static org.kuali.rice.ksb.messaging.serviceconnectors.HttpClientParams.*;
052
053/**
054 * Configures HttpClientBuilder instances for use by the HttpInvokerConnector.
055 *
056 * <p>This class adapts the configuration mechanism which was used with Commons HttpClient, which used a number of
057 * specific Rice config params (see {@link HttpClientParams}) to work with  the HttpComponents HttpClient.  The
058 * configuration doesn't all map across nicely, so coverage is not perfect.</p>
059 *
060 * <p>If the configuration parameters here are not sufficient, this implementation is designed to be extended.</p>
061 *
062 * @author Kuali Rice Team (rice.collab@kuali.org)
063 */
064public class DefaultHttpClientConfigurer implements HttpClientConfigurer, InitializingBean {
065
066    static final Logger LOG = LoggerFactory.getLogger(DefaultHttpClientConfigurer.class);
067
068    private static final String RETRY_SOCKET_EXCEPTION_PROPERTY = "ksb.thinClient.retrySocketException";
069    private static final int DEFAULT_SOCKET_TIMEOUT = 2 * 60 * 1000; // two minutes in milliseconds
070
071    /**
072     * Default maximum total connections per client
073     */
074    private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 20;
075
076    // list of config params starting with "http." to ignore when looking for unsupported params
077    private static final Set<String> unsupportedParamsWhitelist =
078            new HashSet<String>(Arrays.asList("http.port", "http.service.url"));
079
080    /**
081     * Customizes the configuration of the httpClientBuilder.
082     *
083     * <p>Internally, this uses several helper methods to assist with configuring:
084     * <ul>
085     *     <li>Calls {@link #buildConnectionManager()} and sets the resulting {@link HttpClientConnectionManager} (if
086     *     non-null) into the httpClientBuilder.</li>
087     *     <li>Calls {@link #buildRequestConfig()} and sets the resulting {@link RequestConfig} (if non-null) into the
088     *     httpClientBuilder.</li>
089     *     <li>Calls {@link #buildRetryHandler()} and sets the resulting {@link HttpRequestRetryHandler} (if non-null)
090     *     into the httpClientBuilder.</li>
091     * </ul>
092     * </p>
093     *
094     * @param httpClientBuilder the httpClientBuilder being configured
095     */
096    @Override
097    public void customizeHttpClient(HttpClientBuilder httpClientBuilder) {
098
099        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}