View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider.http5;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.net.ProxySelector;
22  import java.security.KeyManagementException;
23  import java.security.KeyStoreException;
24  import java.security.NoSuchAlgorithmException;
25  import java.security.cert.CertificateException;
26  import java.util.ArrayList;
27  import java.util.Arrays;
28  import java.util.Collection;
29  import java.util.Collections;
30  import java.util.List;
31  import java.util.Objects;
32  import java.util.stream.Stream;
33  
34  import javax.net.ssl.HostnameVerifier;
35  import javax.net.ssl.SSLContext;
36  
37  import org.apache.commons.lang3.StringUtils;
38  import org.apache.commons.vfs2.Capability;
39  import org.apache.commons.vfs2.FileName;
40  import org.apache.commons.vfs2.FileSystem;
41  import org.apache.commons.vfs2.FileSystemConfigBuilder;
42  import org.apache.commons.vfs2.FileSystemException;
43  import org.apache.commons.vfs2.FileSystemOptions;
44  import org.apache.commons.vfs2.UserAuthenticationData;
45  import org.apache.commons.vfs2.UserAuthenticator;
46  import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider;
47  import org.apache.commons.vfs2.provider.GenericFileName;
48  import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
49  import org.apache.hc.client5.http.auth.AuthCache;
50  import org.apache.hc.client5.http.auth.AuthScope;
51  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
52  import org.apache.hc.client5.http.classic.HttpClient;
53  import org.apache.hc.client5.http.config.ConnectionConfig;
54  import org.apache.hc.client5.http.cookie.BasicCookieStore;
55  import org.apache.hc.client5.http.cookie.Cookie;
56  import org.apache.hc.client5.http.cookie.CookieStore;
57  import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
58  import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
59  import org.apache.hc.client5.http.impl.auth.BasicScheme;
60  import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
61  import org.apache.hc.client5.http.impl.classic.HttpClients;
62  import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
63  import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
64  import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
65  import org.apache.hc.client5.http.io.HttpClientConnectionManager;
66  import org.apache.hc.client5.http.protocol.HttpClientContext;
67  import org.apache.hc.client5.http.routing.HttpRoutePlanner;
68  import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
69  import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
70  import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
71  import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
72  import org.apache.hc.client5.http.ssl.TrustAllStrategy;
73  import org.apache.hc.core5.http.ConnectionReuseStrategy;
74  import org.apache.hc.core5.http.Header;
75  import org.apache.hc.core5.http.HttpHeaders;
76  import org.apache.hc.core5.http.HttpHost;
77  import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
78  import org.apache.hc.core5.http.io.SocketConfig;
79  import org.apache.hc.core5.http.message.BasicHeader;
80  import org.apache.hc.core5.http.ssl.TLS;
81  import org.apache.hc.core5.ssl.SSLContextBuilder;
82  import org.apache.hc.core5.util.Timeout;
83  
84  /**
85   * {@code FileProvider} implementation using HttpComponents HttpClient v5 library.
86   *
87   * @since 2.5.0
88   */
89  public class Http5FileProvider extends AbstractOriginatingFileProvider {
90  
91      /** Authenticator information. */
92      static final UserAuthenticationData.Type[] AUTHENTICATOR_TYPES =
93              {
94              UserAuthenticationData.USERNAME,
95              UserAuthenticationData.PASSWORD
96              };
97  
98      /** FileProvider capabilities */
99      static final Collection<Capability> CAPABILITIES =
100             Collections.unmodifiableCollection(
101                     Arrays.asList(
102                             Capability.GET_TYPE,
103                             Capability.READ_CONTENT,
104                             Capability.URI,
105                             Capability.GET_LAST_MODIFIED,
106                             Capability.ATTRIBUTES,
107                             Capability.RANDOM_ACCESS_READ,
108                             Capability.DIRECTORY_READ_CONTENT
109                             )
110                     );
111 
112     /**
113      * Constructs a new provider.
114      */
115     public Http5FileProvider() {
116         setFileNameParser(Http5FileNameParser.getInstance());
117     }
118 
119     private HttpClientConnectionManager createConnectionManager(final Http5FileSystemConfigBuilder builder,
120             final FileSystemOptions fileSystemOptions) throws FileSystemException {
121 
122         final ConnectionConfig connectionConfig = ConnectionConfig.custom()
123                 .setConnectTimeout(Timeout.of(builder.getSoTimeoutDuration(fileSystemOptions)))
124                 .build();
125 
126         final SocketConfig socketConfig =
127                 SocketConfig
128                 .custom()
129                 .setSoTimeout(Timeout.of(builder.getSoTimeoutDuration(fileSystemOptions)))
130                 .build();
131 
132         final String[] tlsVersions = builder.getTlsVersions(fileSystemOptions).split("\\s*,\\s*");
133 
134         final TLS[] tlsArray = Stream.of(tlsVersions).filter(Objects::nonNull).map(TLS::valueOf).toArray(TLS[]::new);
135 
136         final SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create()
137                 .setSslContext(createSSLContext(builder, fileSystemOptions))
138                 .setHostnameVerifier(createHostnameVerifier(builder, fileSystemOptions))
139                 .setTlsVersions(tlsArray)
140                 .build();
141 
142         return PoolingHttpClientConnectionManagerBuilder.create()
143                 .setDefaultConnectionConfig(connectionConfig)
144                 .setSSLSocketFactory(sslSocketFactory)
145                 .setMaxConnTotal(builder.getMaxTotalConnections(fileSystemOptions))
146                 .setMaxConnPerRoute(builder.getMaxConnectionsPerHost(fileSystemOptions))
147                 .setDefaultSocketConfig(socketConfig)
148                 .build();
149     }
150 
151     private CookieStore createDefaultCookieStore(final Http5FileSystemConfigBuilder builder,
152             final FileSystemOptions fileSystemOptions) {
153         final CookieStore cookieStore = new BasicCookieStore();
154         final Cookie[] cookies = builder.getCookies(fileSystemOptions);
155 
156         if (cookies != null) {
157             Stream.of(cookies).forEach(cookieStore::addCookie);
158         }
159 
160         return cookieStore;
161     }
162 
163     private HostnameVerifier createHostnameVerifier(final Http5FileSystemConfigBuilder builder, final FileSystemOptions fileSystemOptions) {
164         if (!builder.isHostnameVerificationEnabled(fileSystemOptions)) {
165             return NoopHostnameVerifier.INSTANCE;
166         }
167         return new DefaultHostnameVerifier();
168     }
169 
170     /**
171      * Create an {@link HttpClient} object for an http4 file system.
172      *
173      * @param builder Configuration options builder for http4 provider
174      * @param rootName The root path
175      * @param fileSystemOptions The file system options
176      * @return an {@link HttpClient} object
177      * @throws FileSystemException if an error occurs.
178      */
179     protected HttpClient createHttpClient(final Http5FileSystemConfigBuilder builder, final GenericFileName rootName,
180             final FileSystemOptions fileSystemOptions) throws FileSystemException {
181         return createHttpClientBuilder(builder, rootName, fileSystemOptions).build();
182     }
183 
184     /**
185      * Create an {@link HttpClientBuilder} object. Invoked by {@link #createHttpClient(Http5FileSystemConfigBuilder, GenericFileName, FileSystemOptions)}.
186      *
187      * @param builder Configuration options builder for HTTP4 provider
188      * @param rootName The root path
189      * @param fileSystemOptions The FileSystem options
190      * @return an {@link HttpClientBuilder} object
191      * @throws FileSystemException if an error occurs
192      */
193     protected HttpClientBuilder createHttpClientBuilder(final Http5FileSystemConfigBuilder builder, final GenericFileName rootName,
194             final FileSystemOptions fileSystemOptions) throws FileSystemException {
195         final List<Header> defaultHeaders = new ArrayList<>();
196         defaultHeaders.add(new BasicHeader(HttpHeaders.USER_AGENT, builder.getUserAgent(fileSystemOptions)));
197 
198         final ConnectionReuseStrategy connectionReuseStrategy = builder.isKeepAlive(fileSystemOptions)
199                 ? DefaultConnectionReuseStrategy.INSTANCE
200                 : (request, response, context) -> false;
201 
202         final HttpClientBuilder httpClientBuilder =
203                 HttpClients.custom()
204                 .setRoutePlanner(createHttpRoutePlanner(builder, fileSystemOptions))
205                 .setConnectionManager(createConnectionManager(builder, fileSystemOptions))
206                 .setConnectionReuseStrategy(connectionReuseStrategy)
207                 .setDefaultHeaders(defaultHeaders)
208                 .setDefaultCookieStore(createDefaultCookieStore(builder, fileSystemOptions));
209 
210         if (!builder.getFollowRedirect(fileSystemOptions)) {
211             httpClientBuilder.disableRedirectHandling();
212         }
213 
214         return httpClientBuilder;
215     }
216 
217     /**
218      * Create an {@link HttpClientContext} object for an http4 file system.
219      *
220      * @param builder Configuration options builder for http4 provider
221      * @param rootName The root path
222      * @param fileSystemOptions The FileSystem options
223      * @param authData The {@code UserAuthenticationData} object
224      * @return an {@link HttpClientContext} object
225      */
226     protected HttpClientContext createHttpClientContext(final Http5FileSystemConfigBuilder builder,
227             final GenericFileName rootName, final FileSystemOptions fileSystemOptions,
228             final UserAuthenticationData authData) {
229 
230         final HttpClientContext clientContext = HttpClientContext.create();
231         final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
232         clientContext.setCredentialsProvider(credsProvider);
233 
234         final String username = UserAuthenticatorUtils.toString(UserAuthenticatorUtils.getData(authData,
235                 UserAuthenticationData.USERNAME, UserAuthenticatorUtils.toChar(rootName.getUserName())));
236         final char[] password = UserAuthenticatorUtils.getData(authData,
237                 UserAuthenticationData.PASSWORD, UserAuthenticatorUtils.toChar(rootName.getPassword()));
238 
239         if (!StringUtils.isEmpty(username)) {
240             // set root port
241             credsProvider.setCredentials(new AuthScope(rootName.getHostName(), rootName.getPort()),
242                     new UsernamePasswordCredentials(username, password));
243         }
244 
245         final HttpHost proxyHost = getProxyHttpHost(builder, fileSystemOptions);
246 
247         if (proxyHost != null) {
248             final UserAuthenticator proxyAuth = builder.getProxyAuthenticator(fileSystemOptions);
249 
250             if (proxyAuth != null) {
251                 final UserAuthenticationData proxyAuthData = UserAuthenticatorUtils.authenticate(proxyAuth,
252                     new UserAuthenticationData.Type[] {UserAuthenticationData.USERNAME, UserAuthenticationData.PASSWORD});
253 
254                 if (proxyAuthData != null) {
255                     final UsernamePasswordCredentials proxyCreds = new UsernamePasswordCredentials(
256                             UserAuthenticatorUtils.toString(
257                                     UserAuthenticatorUtils.getData(proxyAuthData, UserAuthenticationData.USERNAME, null)),
258                             UserAuthenticatorUtils.getData(proxyAuthData, UserAuthenticationData.PASSWORD, null));
259 
260                     // set proxy host port
261                     credsProvider.setCredentials(new AuthScope(proxyHost.getHostName(), proxyHost.getPort()),
262                             proxyCreds);
263                 }
264 
265                 if (builder.isPreemptiveAuth(fileSystemOptions)) {
266                     final AuthCache authCache = new BasicAuthCache();
267                     final BasicScheme basicAuth = new BasicScheme();
268                     authCache.put(proxyHost, basicAuth);
269                     clientContext.setAuthCache(authCache);
270                 }
271             }
272         }
273 
274         return clientContext;
275     }
276 
277     private HttpRoutePlanner createHttpRoutePlanner(final Http5FileSystemConfigBuilder builder,
278             final FileSystemOptions fileSystemOptions) {
279         final HttpHost proxyHost = getProxyHttpHost(builder, fileSystemOptions);
280 
281         if (proxyHost != null) {
282             return new DefaultProxyRoutePlanner(proxyHost);
283         }
284 
285         return new SystemDefaultRoutePlanner(ProxySelector.getDefault());
286     }
287 
288     /**
289      * Create {@link SSLContext} for HttpClient. Invoked by {@link #createHttpClientBuilder(Http5FileSystemConfigBuilder, GenericFileName, FileSystemOptions)}.
290      *
291      * @param builder Configuration options builder for HTTP4 provider
292      * @param fileSystemOptions The FileSystem options
293      * @return a {@link SSLContext} for HttpClient
294      * @throws FileSystemException if an error occurs
295      */
296     protected SSLContext createSSLContext(final Http5FileSystemConfigBuilder builder,
297             final FileSystemOptions fileSystemOptions) throws FileSystemException {
298         try {
299             final SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
300             sslContextBuilder.setKeyStoreType(builder.getKeyStoreType(fileSystemOptions));
301 
302             File keystoreFileObject = null;
303             final String keystoreFile = builder.getKeyStoreFile(fileSystemOptions);
304 
305             if (!StringUtils.isEmpty(keystoreFile)) {
306                 keystoreFileObject = new File(keystoreFile);
307             }
308 
309             if (keystoreFileObject != null && keystoreFileObject.exists()) {
310                 final String keystorePass = builder.getKeyStorePass(fileSystemOptions);
311                 final char[] keystorePassChars = keystorePass != null ? keystorePass.toCharArray() : null;
312                 sslContextBuilder.loadTrustMaterial(keystoreFileObject, keystorePassChars, TrustAllStrategy.INSTANCE);
313             } else {
314                 sslContextBuilder.loadTrustMaterial(TrustAllStrategy.INSTANCE);
315             }
316 
317             return sslContextBuilder.build();
318         } catch (final KeyStoreException e) {
319             throw new FileSystemException("Keystore error. " + e.getMessage(), e);
320         } catch (final KeyManagementException e) {
321             throw new FileSystemException("Cannot retrieve keys. " + e.getMessage(), e);
322         } catch (final NoSuchAlgorithmException e) {
323             throw new FileSystemException("Algorithm error. " + e.getMessage(), e);
324         } catch (final CertificateException e) {
325             throw new FileSystemException("Certificate error. " + e.getMessage(), e);
326         } catch (final IOException e) {
327             throw new FileSystemException("Cannot open key file. " + e.getMessage(), e);
328         }
329     }
330 
331     @Override
332     protected FileSystem doCreateFileSystem(final FileName name, final FileSystemOptions fileSystemOptions)
333             throws FileSystemException {
334         final GenericFileName rootName = (GenericFileName) name;
335         UserAuthenticationData authData = null;
336         HttpClient httpClient;
337         HttpClientContext httpClientContext;
338         try {
339             final Http5FileSystemConfigBuilder builder = Http5FileSystemConfigBuilder.getInstance();
340             authData = UserAuthenticatorUtils.authenticate(fileSystemOptions, AUTHENTICATOR_TYPES);
341             httpClientContext = createHttpClientContext(builder, rootName, fileSystemOptions, authData);
342             httpClient = createHttpClient(builder, rootName, fileSystemOptions);
343         } finally {
344             UserAuthenticatorUtils.cleanup(authData);
345         }
346         return new Http5FileSystem(rootName, fileSystemOptions, httpClient, httpClientContext);
347     }
348 
349     @Override
350     public Collection<Capability> getCapabilities() {
351         return CAPABILITIES;
352     }
353 
354     @Override
355     public FileSystemConfigBuilder getConfigBuilder() {
356         return Http5FileSystemConfigBuilder.getInstance();
357     }
358 
359     private HttpHost getProxyHttpHost(final Http5FileSystemConfigBuilder builder,
360             final FileSystemOptions fileSystemOptions) {
361         final String proxyScheme = builder.getProxyScheme(fileSystemOptions);
362         final String proxyHost = builder.getProxyHost(fileSystemOptions);
363         final int proxyPort = builder.getProxyPort(fileSystemOptions);
364 
365         if (!StringUtils.isEmpty(proxyHost) && proxyPort > 0) {
366             return new HttpHost(proxyScheme, proxyHost, proxyPort);
367         }
368 
369         return null;
370     }
371 
372 }