001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider.http5;
018
019import java.io.File;
020import java.io.IOException;
021import java.net.ProxySelector;
022import java.security.KeyManagementException;
023import java.security.KeyStoreException;
024import java.security.NoSuchAlgorithmException;
025import java.security.cert.CertificateException;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.List;
031import java.util.Objects;
032import java.util.stream.Stream;
033
034import javax.net.ssl.HostnameVerifier;
035import javax.net.ssl.SSLContext;
036
037import org.apache.commons.lang3.StringUtils;
038import org.apache.commons.vfs2.Capability;
039import org.apache.commons.vfs2.FileName;
040import org.apache.commons.vfs2.FileSystem;
041import org.apache.commons.vfs2.FileSystemConfigBuilder;
042import org.apache.commons.vfs2.FileSystemException;
043import org.apache.commons.vfs2.FileSystemOptions;
044import org.apache.commons.vfs2.UserAuthenticationData;
045import org.apache.commons.vfs2.UserAuthenticator;
046import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider;
047import org.apache.commons.vfs2.provider.GenericFileName;
048import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
049import org.apache.hc.client5.http.auth.AuthCache;
050import org.apache.hc.client5.http.auth.AuthScope;
051import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
052import org.apache.hc.client5.http.classic.HttpClient;
053import org.apache.hc.client5.http.config.ConnectionConfig;
054import org.apache.hc.client5.http.cookie.BasicCookieStore;
055import org.apache.hc.client5.http.cookie.Cookie;
056import org.apache.hc.client5.http.cookie.CookieStore;
057import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
058import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
059import org.apache.hc.client5.http.impl.auth.BasicScheme;
060import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
061import org.apache.hc.client5.http.impl.classic.HttpClients;
062import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
063import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner;
064import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
065import org.apache.hc.client5.http.io.HttpClientConnectionManager;
066import org.apache.hc.client5.http.protocol.HttpClientContext;
067import org.apache.hc.client5.http.routing.HttpRoutePlanner;
068import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
069import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
070import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
071import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
072import org.apache.hc.client5.http.ssl.TrustAllStrategy;
073import org.apache.hc.core5.http.ConnectionReuseStrategy;
074import org.apache.hc.core5.http.Header;
075import org.apache.hc.core5.http.HttpHeaders;
076import org.apache.hc.core5.http.HttpHost;
077import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
078import org.apache.hc.core5.http.io.SocketConfig;
079import org.apache.hc.core5.http.message.BasicHeader;
080import org.apache.hc.core5.http.ssl.TLS;
081import org.apache.hc.core5.ssl.SSLContextBuilder;
082import org.apache.hc.core5.util.Timeout;
083
084/**
085 * {@code FileProvider} implementation using HttpComponents HttpClient v5 library.
086 *
087 * @since 2.5.0
088 */
089public class Http5FileProvider extends AbstractOriginatingFileProvider {
090
091    /** Authenticator information. */
092    static final UserAuthenticationData.Type[] AUTHENTICATOR_TYPES =
093            {
094            UserAuthenticationData.USERNAME,
095            UserAuthenticationData.PASSWORD
096            };
097
098    /** FileProvider capabilities */
099    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}