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}