View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.caravan.commons.httpasyncclient.impl;
21  
22  import java.io.IOException;
23  import java.security.GeneralSecurityException;
24  
25  import javax.net.ssl.SSLContext;
26  
27  import org.apache.commons.lang3.StringUtils;
28  import org.apache.http.HttpHost;
29  import org.apache.http.auth.AuthScope;
30  import org.apache.http.auth.UsernamePasswordCredentials;
31  import org.apache.http.client.CredentialsProvider;
32  import org.apache.http.client.config.RequestConfig;
33  import org.apache.http.config.Registry;
34  import org.apache.http.config.RegistryBuilder;
35  import org.apache.http.impl.client.BasicCredentialsProvider;
36  import org.apache.http.impl.client.ProxyAuthenticationStrategy;
37  import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
38  import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
39  import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
40  import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
41  import org.apache.http.impl.nio.reactor.IOReactorConfig;
42  import org.apache.http.nio.conn.NoopIOSessionStrategy;
43  import org.apache.http.nio.conn.SchemeIOSessionStrategy;
44  import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
45  import org.apache.http.nio.reactor.ConnectingIOReactor;
46  import org.apache.http.nio.reactor.IOReactorException;
47  import org.jetbrains.annotations.NotNull;
48  import org.jetbrains.annotations.Nullable;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import io.wcm.caravan.commons.httpclient.HttpClientConfig;
53  import io.wcm.caravan.commons.httpclient.impl.helpers.CertificateLoader;
54  
55  /**
56   * Item for {@link HttpAsyncClientFactoryImpl} for each {@link HttpClientConfig} configured.
57   */
58  class HttpAsyncClientItem {
59  
60    private final HttpClientConfig config;
61    private final CloseableHttpAsyncClient httpAsyncClient;
62    private final RequestConfig defaultRequestConfig;
63  
64    private static final Logger log = LoggerFactory.getLogger(HttpAsyncClientItem.class);
65  
66    /**
67     * @param config Http client configuration
68     */
69    HttpAsyncClientItem(@NotNull HttpClientConfig config) {
70      this.config = config;
71  
72      // optional SSL client certificate support
73      SSLContext sslContext;
74      if (CertificateLoader.isSslKeyManagerEnabled(config) || CertificateLoader.isSslTrustStoreEnbaled(config)) {
75        try {
76          sslContext = CertificateLoader.buildSSLContext(config);
77        }
78        catch (IOException | GeneralSecurityException ex) {
79          throw new IllegalArgumentException("Invalid SSL client certificate configuration.", ex);
80        }
81      }
82      else {
83        sslContext = CertificateLoader.createDefaultSSlContext();
84      }
85  
86      CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
87      // optional proxy authentication
88      if (StringUtils.isNotEmpty(config.getProxyUser())) {
89        credentialsProvider.setCredentials(new AuthScope(config.getProxyHost(), config.getProxyPort()),
90            new UsernamePasswordCredentials(config.getProxyUser(), config.getProxyPassword()));
91      }
92      // optional http basic authentication support
93      if (StringUtils.isNotEmpty(config.getHttpUser())) {
94        credentialsProvider.setCredentials(AuthScope.ANY,
95            new UsernamePasswordCredentials(config.getHttpUser(), config.getHttpPassword()));
96      }
97  
98      // build request config
99      defaultRequestConfig = buildDefaultRequestConfig(config);
100 
101     // build http clients
102     PoolingNHttpClientConnectionManager asyncConnectionManager = buildAsyncConnectionManager(config, sslContext);
103     httpAsyncClient = buildHttpAsyncClient(config, asyncConnectionManager, credentialsProvider, defaultRequestConfig);
104 
105     // start async client
106     httpAsyncClient.start();
107   }
108 
109   private static @NotNull RequestConfig buildDefaultRequestConfig(@NotNull HttpClientConfig config) {
110     return RequestConfig.custom()
111             .setConnectionRequestTimeout(config.getConnectionRequestTimeout())
112             .setConnectTimeout(config.getConnectTimeout())
113             .setSocketTimeout(config.getSocketTimeout())
114             .setCookieSpec(config.getCookieSpec())
115             .build();
116   }
117 
118   private static @NotNull PoolingNHttpClientConnectionManager buildAsyncConnectionManager(@NotNull HttpClientConfig config,
119       @NotNull SSLContext sslContext) {
120     // scheme configuration
121     SchemeIOSessionStrategy sslSocketFactory = new SSLIOSessionStrategy(sslContext);
122     Registry<SchemeIOSessionStrategy> asyncSchemeRegistry = RegistryBuilder.<SchemeIOSessionStrategy>create()
123         .register("http", NoopIOSessionStrategy.INSTANCE)
124         .register("https", sslSocketFactory)
125         .build();
126 
127     // pooling settings
128     ConnectingIOReactor ioreactor;
129     try {
130       ioreactor = new DefaultConnectingIOReactor(IOReactorConfig.DEFAULT);
131     }
132     catch (IOReactorException ex) {
133       throw new RuntimeException("Unable to initialize IO reactor.", ex);
134     }
135     PoolingNHttpClientConnectionManager conmgr = new PoolingNHttpClientConnectionManager(ioreactor, asyncSchemeRegistry);
136     conmgr.setMaxTotal(config.getMaxTotalConnections());
137     conmgr.setDefaultMaxPerRoute(config.getMaxConnectionsPerHost());
138     return conmgr;
139   }
140 
141   private static @NotNull CloseableHttpAsyncClient buildHttpAsyncClient(@NotNull HttpClientConfig config,
142       @NotNull PoolingNHttpClientConnectionManager connectionManager, @NotNull CredentialsProvider credentialsProvider,
143       @NotNull RequestConfig defaultRequestConfig) {
144 
145     // prepare HTTPClient builder
146     HttpAsyncClientBuilder httpClientAsyncBuilder = HttpAsyncClientBuilder.create()
147         .setConnectionManager(connectionManager);
148 
149     // timeout settings
150     httpClientAsyncBuilder.setDefaultRequestConfig(defaultRequestConfig);
151 
152     httpClientAsyncBuilder.setDefaultCredentialsProvider(credentialsProvider);
153 
154     // optional proxy support
155     if (StringUtils.isNotEmpty(config.getProxyHost())) {
156       httpClientAsyncBuilder.setProxy(new HttpHost(config.getProxyHost(), config.getProxyPort()));
157 
158       // optional proxy authentication
159       if (StringUtils.isNotEmpty(config.getProxyUser())) {
160         httpClientAsyncBuilder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
161       }
162     }
163 
164     return httpClientAsyncBuilder.build();
165   }
166 
167   /**
168    * @return Http client instance (asynchronous)
169    */
170   public @NotNull CloseableHttpAsyncClient getHttpAsyncClient() {
171     return httpAsyncClient;
172   }
173 
174   /**
175    * @return Default request config
176    */
177   public @NotNull RequestConfig getDefaultRequestConfig() {
178     return defaultRequestConfig;
179   }
180 
181   /**
182    * @param hostName Host name
183    * @param wsAddressingToURI WS addressing "to" URI
184    * @param path Path part of URI
185    * @param isWsCall indicates if the call is a soap webservice call
186    * @return true if host name is associated with this http client config
187    */
188   public boolean matches(@Nullable String hostName, @Nullable String wsAddressingToURI, @Nullable String path, boolean isWsCall) {
189     if (isWsCall) {
190       return config.isEnabled()
191           && config.matchesHost(hostName)
192           && config.matchesPath(path)
193           && config.matchesWsAddressingToUri(wsAddressingToURI);
194     }
195     else {
196       return config.isEnabled()
197           && config.matchesHost(hostName)
198           && config.matchesPath(path);
199     }
200   }
201 
202   /**
203    * Close underlying http clients.
204    */
205   public void close() {
206     try {
207       httpAsyncClient.close();
208     }
209     catch (IOException ex) {
210       log.warn("Error closing async HTTP client.", ex);
211     }
212   }
213 
214   @Override
215   public @NotNull String toString() {
216     return "HttpClientItem[" + config.toString() + "]";
217   }
218 
219 }