View Javadoc
1   /*
2    * This file is part of dependency-check-utils.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2024 Hans Aikema. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.utils;
19  
20  import org.apache.hc.client5.http.HttpResponseException;
21  import org.apache.hc.client5.http.auth.AuthCache;
22  import org.apache.hc.client5.http.auth.AuthScope;
23  import org.apache.hc.client5.http.auth.Credentials;
24  import org.apache.hc.client5.http.auth.CredentialsStore;
25  import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
26  import org.apache.hc.client5.http.impl.auth.BasicAuthCache;
27  import org.apache.hc.client5.http.impl.auth.BasicScheme;
28  import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider;
29  import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler;
30  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
31  import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
32  import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
33  import org.apache.hc.client5.http.protocol.HttpClientContext;
34  import org.apache.hc.core5.http.ClassicHttpResponse;
35  import org.apache.hc.core5.http.ContentType;
36  import org.apache.hc.core5.http.Header;
37  import org.apache.hc.core5.http.HttpEntity;
38  import org.apache.hc.core5.http.HttpException;
39  import org.apache.hc.core5.http.HttpHeaders;
40  import org.apache.hc.core5.http.HttpHost;
41  import org.apache.hc.core5.http.Method;
42  import org.apache.hc.core5.http.io.HttpClientResponseHandler;
43  import org.apache.hc.core5.http.io.entity.BasicHttpEntity;
44  import org.apache.hc.core5.http.io.entity.StringEntity;
45  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
46  import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
47  import org.jetbrains.annotations.NotNull;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import javax.net.ssl.SSLHandshakeException;
52  import java.io.File;
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.net.InetSocketAddress;
56  import java.net.MalformedURLException;
57  import java.net.Proxy;
58  import java.net.ProxySelector;
59  import java.net.SocketAddress;
60  import java.net.URI;
61  import java.net.URISyntaxException;
62  import java.net.URL;
63  import java.nio.charset.Charset;
64  import java.nio.file.Files;
65  import java.nio.file.Path;
66  import java.nio.file.Paths;
67  import java.nio.file.StandardCopyOption;
68  import java.util.ArrayList;
69  import java.util.Collections;
70  import java.util.List;
71  import java.util.Locale;
72  
73  import static java.lang.String.format;
74  
75  /**
76   * A Utility class to centralize download logic like HTTP(S) proxy configuration and proxy- and server credential handling.
77   * @author Jeremy Long, Hans Aikema
78   */
79  public final class Downloader {
80  
81      /**
82       * The builder to use for a HTTP Client that uses the configured proxy-settings
83       */
84      private final HttpClientBuilder httpClientBuilder;
85  
86      /**
87       * The builder to use for a HTTP Client that explicitly opts out of proxy-usage
88       */
89      private final HttpClientBuilder httpClientBuilderExplicitNoproxy;
90  
91      /**
92       * The Authentication cache for pre-emptive authentication.
93       * This gets filled with credentials from the settings in {@link #configure(Settings)}.
94       */
95      private final AuthCache authCache = new BasicAuthCache();
96  
97      /**
98       * The credentialsProvider for pre-emptive authentication.
99       * This gets filled with credentials from the settings in {@link #configure(Settings)}.
100      */
101     private final SystemDefaultCredentialsProvider credentialsProvider = new SystemDefaultCredentialsProvider();
102 
103     /**
104      * The settings
105      */
106     private Settings settings;
107 
108     /**
109      * The Logger for use throughout the class.
110      */
111     private static final Logger LOGGER = LoggerFactory.getLogger(Downloader.class);
112 
113     /**
114      * The singleton instance of the downloader
115      */
116     private static final Downloader INSTANCE = new Downloader();
117     /**
118      * The Credentials for the proxy when proxy authentication is configured in the Settings.
119      */
120     private Credentials proxyCreds = null;
121     /**
122      * A BasicScheme initialized with the proxy-credentials when proxy authentication is configured in the Settings.
123      */
124     private BasicScheme proxyPreEmptAuth = null;
125     /**
126      * The AuthScope for the proxy when proxy authentication is configured in the Settings.
127      */
128     private AuthScope proxyAuthScope = null;
129     /**
130      * The HttpHost for the proxy when proxy authentication is configured in the Settings.
131      */
132     private HttpHost proxyHttpHost = null;
133 
134     private Downloader() {
135         // Singleton class
136         final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
137         //TODO: ensure proper closure and eviction policy
138         httpClientBuilder = HttpClientBuilder.create()
139                 .useSystemProperties()
140                 .setConnectionManager(connectionManager)
141                 .setConnectionManagerShared(true);
142         httpClientBuilderExplicitNoproxy = HttpClientBuilder.create()
143                 .useSystemProperties()
144                 .setConnectionManager(connectionManager)
145                 .setConnectionManagerShared(true)
146                 .setProxySelector(new ProxySelector() {
147                     @Override
148                     public List<Proxy> select(URI uri) {
149                         return Collections.singletonList(Proxy.NO_PROXY);
150                     }
151 
152                     @Override
153                     public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
154 
155                     }
156                 });
157     }
158 
159     /**
160      * The singleton instance for downloading file resources.
161      *
162      * @return The singleton instance managing download credentials and proxy configuration
163      */
164     public static Downloader getInstance() {
165         return INSTANCE;
166     }
167 
168     /**
169      * Initialize the Downloader from the settings.
170      * Extracts the configured proxy- and credential information from the settings and system properties and
171      * caches those for future use by the Downloader.
172      *
173      * @param settings The settings to configure from
174      * @throws InvalidSettingException When improper configurations are found.
175      */
176     public void configure(Settings settings) throws InvalidSettingException {
177         this.settings = settings;
178 
179         if (settings.getString(Settings.KEYS.PROXY_SERVER) != null) {
180             // Legacy proxy configuration present
181             // So don't rely on the system properties for proxy; use the legacy settings configuration
182             final String proxyHost = settings.getString(Settings.KEYS.PROXY_SERVER);
183             final int proxyPort = settings.getInt(Settings.KEYS.PROXY_PORT, -1);
184             final String nonProxyHosts = settings.getString(Settings.KEYS.PROXY_NON_PROXY_HOSTS);
185             if (nonProxyHosts != null && !nonProxyHosts.isEmpty()) {
186                 final ProxySelector selector = new SelectiveProxySelector(
187                         new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)),
188                         nonProxyHosts.split("\\|")
189                 );
190                 httpClientBuilder.setProxySelector(selector);
191             } else {
192                 httpClientBuilder.setProxy(new HttpHost(proxyHost, proxyPort));
193             }
194             if (settings.getString(Settings.KEYS.PROXY_USERNAME) != null) {
195                 final String proxyuser = settings.getString(Settings.KEYS.PROXY_USERNAME);
196                 final char[] proxypass = settings.getString(Settings.KEYS.PROXY_PASSWORD).toCharArray();
197                 this.proxyHttpHost = new HttpHost(null, proxyHost, proxyPort);
198                 this.proxyCreds = new UsernamePasswordCredentials(proxyuser, proxypass);
199                 this.proxyAuthScope = new AuthScope(proxyHttpHost);
200                 this.proxyPreEmptAuth = new BasicScheme();
201                 this.proxyPreEmptAuth.initPreemptive(proxyCreds);
202                 tryConfigureProxyCredentials(credentialsProvider, authCache);
203             }
204         }
205         tryAddRetireJSCredentials();
206         tryAddHostedSuppressionCredentials();
207         tryAddKEVCredentials();
208         tryAddNexusAnalyzerCredentials();
209         tryAddCentralAnalyzerCredentials();
210         tryAddCentralContentCredentials();
211         tryAddNVDApiDatafeed();
212         httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
213         httpClientBuilderExplicitNoproxy.setDefaultCredentialsProvider(credentialsProvider);
214     }
215 
216     private void tryAddRetireJSCredentials() throws InvalidSettingException {
217         if (!settings.getString(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, "").isBlank()) {
218             configureCredentials(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, "RetireJS repo.js",
219                     Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_USER, Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_PASSWORD,
220                     Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_BEARER_TOKEN
221                     );
222         }
223     }
224 
225     private void tryAddHostedSuppressionCredentials() throws InvalidSettingException {
226         if (!settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, "").isBlank()) {
227             configureCredentials(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, "Hosted suppressions",
228                     Settings.KEYS.HOSTED_SUPPRESSIONS_USER, Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD,
229                     Settings.KEYS.HOSTED_SUPPRESSIONS_BEARER_TOKEN
230             );
231         }
232     }
233 
234     private void tryAddKEVCredentials() throws InvalidSettingException {
235         if (!settings.getString(Settings.KEYS.KEV_URL, "").isBlank()) {
236             configureCredentials(Settings.KEYS.KEV_URL, "Known Exploited Vulnerabilities",
237                     Settings.KEYS.KEV_USER, Settings.KEYS.KEV_PASSWORD,
238                     Settings.KEYS.KEV_BEARER_TOKEN
239             );
240         }
241     }
242 
243     private void tryAddNexusAnalyzerCredentials() throws InvalidSettingException {
244         if (!settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL, "").isBlank()) {
245             configureCredentials(Settings.KEYS.ANALYZER_NEXUS_URL, "Nexus Analyzer",
246                     Settings.KEYS.ANALYZER_NEXUS_USER, Settings.KEYS.ANALYZER_NEXUS_PASSWORD,
247                     null
248             );
249         }
250     }
251 
252     private void tryAddCentralAnalyzerCredentials() throws InvalidSettingException {
253         if (!settings.getString(Settings.KEYS.ANALYZER_CENTRAL_URL, "").isBlank()) {
254             configureCredentials(Settings.KEYS.ANALYZER_CENTRAL_URL, "Central Analyzer",
255                     Settings.KEYS.ANALYZER_CENTRAL_USER, Settings.KEYS.ANALYZER_CENTRAL_PASSWORD,
256                     Settings.KEYS.ANALYZER_CENTRAL_BEARER_TOKEN
257             );
258         }
259     }
260 
261     private void tryAddCentralContentCredentials() throws InvalidSettingException {
262         if (!settings.getString(Settings.KEYS.CENTRAL_CONTENT_URL, "").isBlank()) {
263             configureCredentials(Settings.KEYS.CENTRAL_CONTENT_URL, "Central Content",
264                     Settings.KEYS.CENTRAL_CONTENT_USER, Settings.KEYS.CENTRAL_CONTENT_PASSWORD,
265                     Settings.KEYS.CENTRAL_CONTENT_BEARER_TOKEN
266 
267             );
268         }
269     }
270 
271     private void tryAddNVDApiDatafeed() throws InvalidSettingException {
272         if (!settings.getString(Settings.KEYS.NVD_API_DATAFEED_URL, "").isBlank()) {
273             configureCredentials(Settings.KEYS.NVD_API_DATAFEED_URL, "NVD API Datafeed",
274                     Settings.KEYS.NVD_API_DATAFEED_USER, Settings.KEYS.NVD_API_DATAFEED_PASSWORD,
275                     Settings.KEYS.NVD_API_DATAFEED_BEARER_TOKEN
276             );
277         }
278     }
279 
280     /**
281      * Configure pre-emptive credentials for the host/port of the URL when configured in settings for the default credential-store and
282      * authentication-cache.
283      *
284      * @param urlKey           The settings property key for a configured url for which the credentials should hold
285      * @param scopeDescription A descriptive text for use in error messages for this credential
286      * @param userKey          The settings property key for a potentially configured configured Basic-auth username
287      * @param passwordKey      The settings property key for a potentially configured configured Basic-auth password
288      * @param tokenKey         The settings property key for a potentially configured Bearer-auth token
289      * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings.
290      */
291     private void configureCredentials(String urlKey, String scopeDescription, String userKey, String passwordKey, String tokenKey)
292             throws InvalidSettingException {
293         final URL theURL;
294         try {
295             theURL = new URL(settings.getString(urlKey, ""));
296         } catch (MalformedURLException e) {
297             throw new InvalidSettingException(scopeDescription + " URL must be a valid URL (was: " + settings.getString(urlKey, "") + ")", e);
298         }
299         configureCredentials(theURL, scopeDescription, userKey, passwordKey, tokenKey, credentialsProvider, authCache);
300     }
301 
302     /**
303      * Configure pre-emptive credentials for the host/port of the URL when configured in settings for a specific credential-store and
304      * authentication-cache.
305      *
306      * @param theURL      The url for which the credentials should hold
307      * @param scopeDescription        A descriptive text for use in error messages for this credential
308      * @param userKey     The settings property key for a potentially configured configured Basic-auth username
309      * @param passwordKey The settings property key for a potentially configured configured Basic-auth password
310      * @param tokenKey The settings property key for a potentially configured Bearer-auth token
311      * @param theCredentialsStore The credential store that will be set in the HTTP clients context
312      * @param theAuthCache        The authentication cache that will be set in the HTTP clients context
313      * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings.
314      */
315     private void configureCredentials(URL theURL, String scopeDescription, String userKey, String passwordKey, String tokenKey,
316                                       CredentialsStore theCredentialsStore, AuthCache theAuthCache)
317             throws InvalidSettingException {
318         final String theUser = settings.getString(userKey);
319         final String thePass = settings.getString(passwordKey);
320         final String theToken = tokenKey != null ? settings.getString(tokenKey) : null;
321         if (theUser == null && thePass == null && theToken == null) {
322             // no credentials configured
323             return;
324         }
325         final String theProtocol = theURL.getProtocol();
326         if ("file".equals(theProtocol)) {
327             // no credentials support for file protocol
328             return;
329         } else if ("http".equals(theProtocol) && (theUser != null && thePass != null)) {
330             LOGGER.warn("Insecure configuration: Basic Credentials are configured to be used over a plain http connection for {}. "
331                     + "Consider migrating to https to guard the credentials.", scopeDescription);
332         } else if ("http".equals(theProtocol) && (theToken != null)) {
333             LOGGER.warn("Insecure configuration: Bearer Credentials are configured to be used over a plain http connection for {}. "
334                     + "Consider migrating to https to guard the credentials.", scopeDescription);
335         } else if (!"https".equals(theProtocol)) {
336             throw new InvalidSettingException("Unsupported protocol in the " + scopeDescription
337                     + " URL; only file, http and https are supported");
338         }
339         if (theToken != null) {
340             HC5CredentialHelper.configurePreEmptiveBearerAuth(theURL, theToken, theCredentialsStore, theAuthCache);
341         } else if (theUser != null && thePass != null) {
342             HC5CredentialHelper.configurePreEmptiveBasicAuth(theURL, theUser, thePass, theCredentialsStore, theAuthCache);
343         }
344     }
345 
346     /**
347      * Retrieves a file from a given URL and saves it to the outputPath.
348      *
349      * @param url        the URL of the file to download
350      * @param outputPath the path to the save the file to
351      * @throws DownloadFailedException       is thrown if there is an error downloading the file
352      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
353      * @throws TooManyRequestsException      thrown when a 429 is received
354      * @throws ResourceNotFoundException     thrown when a 404 is received
355      */
356     public void fetchFile(URL url, File outputPath)
357             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
358         fetchFile(url, outputPath, true);
359     }
360 
361     /**
362      * Retrieves a file from a given URL and saves it to the outputPath.
363      *
364      * @param url        the URL of the file to download
365      * @param outputPath the path to the save the file to
366      * @param useProxy   whether to use the configured proxy when downloading
367      *                   files
368      * @throws DownloadFailedException       is thrown if there is an error downloading the file
369      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
370      * @throws TooManyRequestsException      thrown when a 429 is received
371      * @throws ResourceNotFoundException     thrown when a 404 is received
372      */
373     public void fetchFile(URL url, File outputPath, boolean useProxy) throws DownloadFailedException,
374             TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
375         try {
376             if ("file".equals(url.getProtocol())) {
377                 final Path p = Paths.get(url.toURI());
378                 Files.copy(p, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING);
379             } else {
380                 final BasicClassicHttpRequest req;
381                 req = new BasicClassicHttpRequest(Method.GET, url.toURI());
382                 try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
383                     final SaveToFileResponseHandler responseHandler = new SaveToFileResponseHandler(outputPath);
384                     hc.execute(req, getPreEmptiveAuthContext(), responseHandler);
385                 }
386             }
387         } catch (HttpResponseException hre) {
388             wrapAndThrowHttpResponseException(url.toString(), hre);
389         } catch (SSLHandshakeException ex) {
390             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
391                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
392                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
393                 throw new URLConnectionFailureException(msg, ex);
394             }
395             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
396             throw new DownloadFailedException(msg, ex);
397         } catch (RuntimeException | URISyntaxException | IOException ex) {
398             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
399             throw new DownloadFailedException(msg, ex);
400         }
401     }
402 
403     private static void wrapAndThrowHttpResponseException(String url, HttpResponseException hre)
404             throws ResourceNotFoundException, TooManyRequestsException, DownloadFailedException {
405         final String messageFormat = "%s - Server status: %d - Server reason: %s";
406         switch (hre.getStatusCode()) {
407             case 404:
408                 throw new ResourceNotFoundException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
409             case 429:
410                 throw new TooManyRequestsException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
411             default:
412                 throw new DownloadFailedException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
413         }
414     }
415 
416     /**
417      * Retrieves a file from a given URL using an ad-hoc created CredentialsProvider if needed
418      * and saves it to the outputPath.
419      *
420      * @param url         the URL of the file to download
421      * @param outputPath  the path to the save the file to
422      * @param useProxy    whether to use the configured proxy when downloading files
423      * @param userKey     The settings property key for a potentially configured configured Basic-auth username
424      * @param passwordKey The settings property key for a potentially configured configured Basic-auth password
425      * @param tokenKey    The settings property key for a potentially configured Bearer-auth token
426      * @throws DownloadFailedException       is thrown if there is an error downloading the file
427      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
428      * @throws TooManyRequestsException      thrown when a 429 is received
429      * @throws ResourceNotFoundException     thrown when a 404 is received
430      * @implNote This method should only be used in cases where the target host cannot be determined beforehand from settings, so that ad-hoc
431      * Credentials needs to be constructed for the target URL when the user/password keys point to configured credentials. The method delegates to
432      * {@link #fetchFile(URL, File, boolean)} when credentials are not configured for the given keys or the resource points to a file.
433      */
434     public void fetchFile(URL url, File outputPath, boolean useProxy, String userKey, String passwordKey, String tokenKey)
435             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
436         final boolean basicConfigured = userKey != null && settings.getString(userKey) != null
437                 && passwordKey != null && settings.getString(passwordKey) != null;
438         final boolean tokenConfigured = tokenKey != null && settings.getString(tokenKey) != null;
439         if ("file".equals(url.getProtocol()) || (!basicConfigured && !tokenConfigured)) {
440             // no credentials configured, so use the default fetchFile
441             fetchFile(url, outputPath, useProxy);
442             return;
443         }
444         final String theProtocol = url.getProtocol();
445         if (!("http".equals(theProtocol) || "https".equals(theProtocol))) {
446             throw new DownloadFailedException("Unsupported protocol in the URL; only file, http and https are supported");
447         }
448         try {
449             final HttpClientContext dedicatedAuthContext = HttpClientContext.create();
450             final CredentialsStore dedicatedCredentialStore = new SystemDefaultCredentialsProvider();
451             final AuthCache dedicatedAuthCache = new BasicAuthCache();
452             configureCredentials(url, url.toString(), userKey, passwordKey, tokenKey, dedicatedCredentialStore, dedicatedAuthCache);
453             if (useProxy && proxyAuthScope != null) {
454                 tryConfigureProxyCredentials(dedicatedCredentialStore, dedicatedAuthCache);
455             }
456             dedicatedAuthContext.setCredentialsProvider(dedicatedCredentialStore);
457             dedicatedAuthContext.setAuthCache(dedicatedAuthCache);
458             try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
459                 final BasicClassicHttpRequest req = new BasicClassicHttpRequest(Method.GET, url.toURI());
460                 final SaveToFileResponseHandler responseHandler = new SaveToFileResponseHandler(outputPath);
461                 hc.execute(req, dedicatedAuthContext, responseHandler);
462             }
463         } catch (HttpResponseException hre) {
464             wrapAndThrowHttpResponseException(url.toString(), hre);
465         } catch (SSLHandshakeException ex) {
466             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
467                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
468                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
469                 throw new URLConnectionFailureException(msg, ex);
470             }
471             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
472             throw new DownloadFailedException(msg, ex);
473         } catch (RuntimeException | URISyntaxException | IOException ex) {
474             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
475             throw new DownloadFailedException(msg, ex);
476         }
477     }
478 
479     /**
480      * Add the proxy credentials to the CredentialsProvider and AuthCache instances when proxy-authentication is configured in the settings.
481      * @param credentialsProvider The credentialStore to configure the credentials in
482      * @param authCache The AuthCache to cache the pre-empted credentials in
483      */
484     private void tryConfigureProxyCredentials(@NotNull CredentialsStore credentialsProvider, @NotNull AuthCache authCache) {
485         if (proxyPreEmptAuth != null) {
486             credentialsProvider.setCredentials(proxyAuthScope, proxyCreds);
487             authCache.put(proxyHttpHost, proxyPreEmptAuth);
488         }
489     }
490 
491     /**
492      * Posts a payload to the URL and returns the response as a string.
493      *
494      * @param url         the URL to POST to
495      * @param payload     the Payload to post
496      * @param payloadType the string describing the payload's mime-type
497      * @param hdr         Additional headers to add to the HTTP request
498      * @return the content of the response
499      * @throws DownloadFailedException       is thrown if there is an error downloading the file
500      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
501      * @throws TooManyRequestsException      thrown when a 429 is received
502      * @throws ResourceNotFoundException     thrown when a 404 is received
503      */
504     public String postBasedFetchContent(URI url, String payload, ContentType payloadType, List<Header> hdr)
505             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
506         try {
507             if (url.getScheme() == null || !url.getScheme().toLowerCase(Locale.ROOT).matches("^https?")) {
508                 throw new IllegalArgumentException("Unsupported protocol in the URL; only http and https are supported");
509             } else {
510                 final BasicClassicHttpRequest req;
511                 req = new BasicClassicHttpRequest(Method.POST, url);
512                 req.setEntity(new StringEntity(payload, payloadType));
513                 for (Header h : hdr) {
514                     req.addHeader(h);
515                 }
516                 final String result;
517                 try (CloseableHttpClient hc = httpClientBuilder.build()) {
518                     result = hc.execute(req, getPreEmptiveAuthContext(), new BasicHttpClientResponseHandler());
519                 }
520                 return result;
521             }
522         } catch (HttpResponseException hre) {
523             wrapAndThrowHttpResponseException(url.toString(), hre);
524             throw new InternalError("wrapAndThrowHttpResponseException will always throw an exception but Java compiler fails to spot it");
525         } catch (SSLHandshakeException ex) {
526             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
527                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
528                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
529                 throw new URLConnectionFailureException(msg, ex);
530             }
531             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
532             throw new DownloadFailedException(msg, ex);
533         } catch (IOException | RuntimeException ex) {
534             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
535             throw new DownloadFailedException(msg, ex);
536         }
537     }
538 
539     /**
540      * Retrieves a file from a given URL and returns the contents.
541      *
542      * @param url     the URL of the file to download
543      * @param charset The characterset to use to interpret the binary content of the file
544      * @return the content of the file
545      * @throws DownloadFailedException   is thrown if there is an error
546      *                                   downloading the file
547      * @throws TooManyRequestsException  thrown when a 429 is received
548      * @throws ResourceNotFoundException thrown when a 404 is received
549      */
550     public String fetchContent(URL url, Charset charset) throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException {
551         return fetchContent(url, true, charset);
552     }
553 
554     /**
555      * Retrieves a file from a given URL and returns the contents.
556      *
557      * @param url      the URL of the file to download
558      * @param useProxy whether to use the configured proxy when downloading
559      *                 files
560      * @param charset  The characterset to use to interpret the binary content of the file
561      * @return the content of the file
562      * @throws DownloadFailedException   is thrown if there is an error
563      *                                   downloading the file
564      * @throws TooManyRequestsException  thrown when a 429 is received
565      * @throws ResourceNotFoundException thrown when a 404 is received
566      */
567     public String fetchContent(URL url, boolean useProxy, Charset charset)
568             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException {
569         try {
570             final String result;
571             if ("file".equals(url.getProtocol())) {
572                 final Path p = Paths.get(url.toURI());
573                 result = Files.readString(p, charset);
574             } else {
575                 final BasicClassicHttpRequest req;
576                 req = new BasicClassicHttpRequest(Method.GET, url.toURI());
577                 try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
578                     req.addHeader(HttpHeaders.ACCEPT_CHARSET, charset.name());
579                     final ExplicitCharsetToStringResponseHandler responseHandler = new ExplicitCharsetToStringResponseHandler(charset);
580                     result = hc.execute(req, getPreEmptiveAuthContext(), responseHandler);
581                 }
582             }
583             return result;
584         } catch (HttpResponseException hre) {
585             wrapAndThrowHttpResponseException(url.toString(), hre);
586             throw new InternalError("wrapAndThrowHttpResponseException will always throw an exception but Java compiler fails to spot it");
587         } catch (RuntimeException | URISyntaxException | IOException ex) {
588             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
589             throw new DownloadFailedException(msg, ex);
590         }
591     }
592 
593     /**
594      * Gets a HttpClientContext that supports pre-emptive authentication.
595      * @return A HttpClientContext pre-configured with the authentication cache build from the settings.
596      */
597     public HttpClientContext getPreEmptiveAuthContext() {
598         final HttpClientContext context = HttpClientContext.create();
599         context.setCredentialsProvider(credentialsProvider);
600         context.setAuthCache(authCache);
601         return context;
602     }
603 
604     /**
605      * Gets a pre-configured HttpClient.
606      * Mainly targeted for use in paged resultset scenarios with multiple roundtrips.
607      * @param useProxy Whether to use the configuration that includes proxy-settings
608      * @return A HttpClient pre-configured with the settings.
609      */
610     public CloseableHttpClient getHttpClient(boolean useProxy) {
611         return useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build();
612     }
613 
614     /**
615      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
616      *
617      * @param url             The url of the resource
618      * @param handler   The responsehandler to handle the response
619      * @param <T>             The return-type for the responseHandler
620      * @return The response handler result
621      * @throws IOException               on I/O Exceptions
622      * @throws TooManyRequestsException  When HTTP status 429 is encountered
623      * @throws ResourceNotFoundException When HTTP status 404 is encountered
624      */
625     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler)
626             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
627         return fetchAndHandle(url, handler, Collections.emptyList(), true);
628     }
629 
630     /**
631      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
632      *
633      * @param url               The url of the resource
634      * @param handler   The responsehandler to handle the response
635      * @param hdr Additional headers to add to the HTTP request
636      * @param <T>               The return-type for the responseHandler
637      * @return The response handler result
638      * @throws IOException               on I/O Exceptions
639      * @throws TooManyRequestsException  When HTTP status 429 is encountered
640      * @throws ResourceNotFoundException When HTTP status 404 is encountered
641      */
642     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler, @NotNull List<Header> hdr)
643             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
644         return fetchAndHandle(url, handler, hdr, true);
645     }
646 
647     /**
648      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
649      *
650      * @param url               The url of the resource
651      * @param handler   The responsehandler to handle the response
652      * @param hdr Additional headers to add to the HTTP request
653      * @param useProxy          Whether to use the configured proxy for the connection
654      * @param <T>               The return-type for the responseHandler
655      * @return The response handler result
656      * @throws IOException               on I/O Exceptions
657      * @throws TooManyRequestsException  When HTTP status 429 is encountered
658      * @throws ResourceNotFoundException When HTTP status 404 is encountered
659      */
660     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler, @NotNull List<Header> hdr, boolean useProxy)
661             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
662         final T data;
663         if ("file".equals(url.getProtocol())) {
664             final Path p = Paths.get(url.toURI());
665             try (InputStream is = Files.newInputStream(p)) {
666                 final HttpEntity dummyEntity = new BasicHttpEntity(is, ContentType.APPLICATION_JSON);
667                 final ClassicHttpResponse dummyResponse = new BasicClassicHttpResponse(200);
668                 dummyResponse.setEntity(dummyEntity);
669                 data = handler.handleResponse(dummyResponse);
670             } catch (HttpException e) {
671                 throw new IllegalStateException("HttpException encountered emulating a HTTP response from a file", e);
672             }
673         } else {
674             try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
675                 return fetchAndHandle(hc, url, handler, hdr);
676             }
677         }
678         return data;
679     }
680     /**
681      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
682      *
683      * @param client            The HTTP Client to reuse for the request
684      * @param url               The url of the resource
685      * @param handler   The responsehandler to handle the response
686      * @param hdr Additional headers to add to the HTTP request
687      * @param <T>               The return-type for the responseHandler
688      * @return The response handler result
689      * @throws IOException               on I/O Exceptions
690      * @throws TooManyRequestsException  When HTTP status 429 is encountered
691      * @throws ResourceNotFoundException When HTTP status 404 is encountered
692      */
693     public <T> T fetchAndHandle(@NotNull CloseableHttpClient client, @NotNull URL url, @NotNull HttpClientResponseHandler<T> handler,
694                                 @NotNull List<Header> hdr) throws IOException, TooManyRequestsException, ResourceNotFoundException {
695         try {
696             final String theProtocol = url.getProtocol();
697             if (!("http".equals(theProtocol) || "https".equals(theProtocol))) {
698                 throw new DownloadFailedException("Unsupported protocol in the URL; only http and https are supported");
699             }
700             final BasicClassicHttpRequest req = new BasicClassicHttpRequest(Method.GET, url.toURI());
701             for (Header h : hdr) {
702                 req.addHeader(h);
703             }
704             final HttpClientContext context = getPreEmptiveAuthContext();
705             return client.execute(req, context, handler);
706         } catch (HttpResponseException hre) {
707             final String messageFormat = "%s - Server status: %d - Server reason: %s";
708             switch (hre.getStatusCode()) {
709                 case 404:
710                     throw new ResourceNotFoundException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
711                 case 429:
712                     throw new TooManyRequestsException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
713                 default:
714                     throw new IOException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
715             }
716         } catch (RuntimeException | URISyntaxException ex) {
717             final String msg = format("Download failed, unable to retrieve and parse '%s'; %s", url, ex.getMessage());
718             throw new IOException(msg, ex);
719         }
720     }
721 
722     private static class SelectiveProxySelector extends ProxySelector {
723 
724         /**
725          * The suffix-match entries from the nonProxyHosts (those starting with a {@code *}).
726          */
727         private final List<String> suffixMatch = new ArrayList<>();
728         /**
729          * The full host entries from the nonProxyHosts (those <em>not</em> starting with a {@code *}).
730          */
731         private final List<String> fullmatch = new ArrayList<>();
732         /**
733          * The proxy use when no proxy-exception is found.
734          */
735         private final Proxy configuredProxy;
736 
737         SelectiveProxySelector(Proxy httpHost, String[] nonProxyHostsPatterns) {
738             for (String nonProxyHostPattern : nonProxyHostsPatterns) {
739                 if (nonProxyHostPattern.startsWith("*")) {
740                     suffixMatch.add(nonProxyHostPattern.substring(1));
741                 } else {
742                     fullmatch.add(nonProxyHostPattern);
743                 }
744             }
745             this.configuredProxy = httpHost;
746         }
747 
748         @Override
749         public List<Proxy> select(URI uri) {
750             final String theHost = uri.getHost();
751             if (fullmatch.contains(theHost)) {
752                 return Collections.singletonList(Proxy.NO_PROXY);
753             } else {
754                 for (String suffix : suffixMatch) {
755                     if (theHost.endsWith(suffix)) {
756                         return Collections.singletonList(Proxy.NO_PROXY);
757                     }
758                 }
759             }
760             return List.of(configuredProxy);
761         }
762 
763         @Override
764         public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
765             // nothing to be done for this single proxy proxy-selector
766         }
767     }
768 }