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         tryAddArtifactoryCredentials();
210         tryAddCentralAnalyzerCredentials();
211         tryAddCentralContentCredentials();
212         tryAddNVDApiDatafeed();
213         httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
214         httpClientBuilderExplicitNoproxy.setDefaultCredentialsProvider(credentialsProvider);
215     }
216 
217     private void tryAddRetireJSCredentials() throws InvalidSettingException {
218         if (!settings.getString(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, "").isBlank()) {
219             configureCredentials(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, "RetireJS repo.js",
220                     Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_USER, Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_PASSWORD,
221                     Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_BEARER_TOKEN
222                     );
223         }
224     }
225 
226     private void tryAddHostedSuppressionCredentials() throws InvalidSettingException {
227         if (!settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, "").isBlank()) {
228             configureCredentials(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, "Hosted suppressions",
229                     Settings.KEYS.HOSTED_SUPPRESSIONS_USER, Settings.KEYS.HOSTED_SUPPRESSIONS_PASSWORD,
230                     Settings.KEYS.HOSTED_SUPPRESSIONS_BEARER_TOKEN
231             );
232         }
233     }
234 
235     private void tryAddKEVCredentials() throws InvalidSettingException {
236         if (!settings.getString(Settings.KEYS.KEV_URL, "").isBlank()) {
237             configureCredentials(Settings.KEYS.KEV_URL, "Known Exploited Vulnerabilities",
238                     Settings.KEYS.KEV_USER, Settings.KEYS.KEV_PASSWORD,
239                     Settings.KEYS.KEV_BEARER_TOKEN
240             );
241         }
242     }
243 
244     private void tryAddNexusAnalyzerCredentials() throws InvalidSettingException {
245         if (!settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL, "").isBlank()) {
246             configureCredentials(Settings.KEYS.ANALYZER_NEXUS_URL, "Nexus Analyzer",
247                     Settings.KEYS.ANALYZER_NEXUS_USER, Settings.KEYS.ANALYZER_NEXUS_PASSWORD,
248                     null
249             );
250         }
251     }
252 
253     private void tryAddCentralAnalyzerCredentials() throws InvalidSettingException {
254         if (!settings.getString(Settings.KEYS.ANALYZER_CENTRAL_URL, "").isBlank()) {
255             configureCredentials(Settings.KEYS.ANALYZER_CENTRAL_URL, "Central Analyzer",
256                     Settings.KEYS.ANALYZER_CENTRAL_USER, Settings.KEYS.ANALYZER_CENTRAL_PASSWORD,
257                     Settings.KEYS.ANALYZER_CENTRAL_BEARER_TOKEN
258             );
259         }
260     }
261 
262     private void tryAddArtifactoryCredentials() throws InvalidSettingException {
263         if (!settings.getString(Settings.KEYS.ANALYZER_ARTIFACTORY_URL, "").isBlank()) {
264             configureCredentials(Settings.KEYS.ANALYZER_ARTIFACTORY_URL, "Artifactory Analyzer",
265                     Settings.KEYS.ANALYZER_ARTIFACTORY_API_USERNAME, Settings.KEYS.ANALYZER_ARTIFACTORY_API_TOKEN,
266                     Settings.KEYS.ANALYZER_ARTIFACTORY_BEARER_TOKEN
267             );
268         }
269     }
270 
271     private void tryAddCentralContentCredentials() throws InvalidSettingException {
272         if (!settings.getString(Settings.KEYS.CENTRAL_CONTENT_URL, "").isBlank()) {
273             configureCredentials(Settings.KEYS.CENTRAL_CONTENT_URL, "Central Content",
274                     Settings.KEYS.CENTRAL_CONTENT_USER, Settings.KEYS.CENTRAL_CONTENT_PASSWORD,
275                     Settings.KEYS.CENTRAL_CONTENT_BEARER_TOKEN
276 
277             );
278         }
279     }
280 
281     private void tryAddNVDApiDatafeed() throws InvalidSettingException {
282         if (!settings.getString(Settings.KEYS.NVD_API_DATAFEED_URL, "").isBlank()) {
283             configureCredentials(Settings.KEYS.NVD_API_DATAFEED_URL, "NVD API Datafeed",
284                     Settings.KEYS.NVD_API_DATAFEED_USER, Settings.KEYS.NVD_API_DATAFEED_PASSWORD,
285                     Settings.KEYS.NVD_API_DATAFEED_BEARER_TOKEN
286             );
287         }
288     }
289 
290     /**
291      * Configure pre-emptive credentials for the host/port of the URL when configured in settings for the default credential-store and
292      * authentication-cache.
293      *
294      * @param urlKey           The settings property key for a configured url for which the credentials should hold
295      * @param scopeDescription A descriptive text for use in error messages for this credential
296      * @param userKey          The settings property key for a potentially configured configured Basic-auth username
297      * @param passwordKey      The settings property key for a potentially configured configured Basic-auth password
298      * @param tokenKey         The settings property key for a potentially configured Bearer-auth token
299      * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings.
300      */
301     private void configureCredentials(String urlKey, String scopeDescription, String userKey, String passwordKey, String tokenKey)
302             throws InvalidSettingException {
303         final URL theURL;
304         try {
305             theURL = new URL(settings.getString(urlKey, ""));
306         } catch (MalformedURLException e) {
307             throw new InvalidSettingException(scopeDescription + " URL must be a valid URL (was: " + settings.getString(urlKey, "") + ")", e);
308         }
309         configureCredentials(theURL, scopeDescription, userKey, passwordKey, tokenKey, credentialsProvider, authCache);
310     }
311 
312     /**
313      * Configure pre-emptive credentials for the host/port of the URL when configured in settings for a specific credential-store and
314      * authentication-cache.
315      *
316      * @param theURL      The url for which the credentials should hold
317      * @param scopeDescription        A descriptive text for use in error messages for this credential
318      * @param userKey     The settings property key for a potentially configured configured Basic-auth username
319      * @param passwordKey The settings property key for a potentially configured configured Basic-auth password
320      * @param tokenKey The settings property key for a potentially configured Bearer-auth token
321      * @param theCredentialsStore The credential store that will be set in the HTTP clients context
322      * @param theAuthCache        The authentication cache that will be set in the HTTP clients context
323      * @throws InvalidSettingException When the password is empty or one of the other keys are not found in the settings.
324      */
325     private void configureCredentials(URL theURL, String scopeDescription, String userKey, String passwordKey, String tokenKey,
326                                       CredentialsStore theCredentialsStore, AuthCache theAuthCache)
327             throws InvalidSettingException {
328         final String theUser = settings.getString(userKey);
329         final String thePass = settings.getString(passwordKey);
330         final String theToken = tokenKey != null ? settings.getString(tokenKey) : null;
331         if (theUser == null && thePass == null && theToken == null) {
332             // no credentials configured
333             return;
334         }
335         final String theProtocol = theURL.getProtocol();
336         if ("file".equals(theProtocol)) {
337             // no credentials support for file protocol
338             return;
339         } else if ("http".equals(theProtocol) && (theUser != null && thePass != null)) {
340             LOGGER.warn("Insecure configuration: Basic Credentials are configured to be used over a plain http connection for {}. "
341                     + "Consider migrating to https to guard the credentials.", scopeDescription);
342         } else if ("http".equals(theProtocol) && (theToken != null)) {
343             LOGGER.warn("Insecure configuration: Bearer Credentials are configured to be used over a plain http connection for {}. "
344                     + "Consider migrating to https to guard the credentials.", scopeDescription);
345         } else if (!"https".equals(theProtocol)) {
346             throw new InvalidSettingException("Unsupported protocol in the " + scopeDescription
347                     + " URL; only file, http and https are supported");
348         }
349         if (theToken != null) {
350             HC5CredentialHelper.configurePreEmptiveBearerAuth(theURL, theToken, theCredentialsStore, theAuthCache);
351         } else if (theUser != null && thePass != null) {
352             HC5CredentialHelper.configurePreEmptiveBasicAuth(theURL, theUser, thePass, theCredentialsStore, theAuthCache);
353         }
354     }
355 
356     /**
357      * Retrieves a file from a given URL and saves it to the outputPath.
358      *
359      * @param url        the URL of the file to download
360      * @param outputPath the path to the save the file to
361      * @throws DownloadFailedException       is thrown if there is an error downloading the file
362      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
363      * @throws TooManyRequestsException      thrown when a 429 is received
364      * @throws ResourceNotFoundException     thrown when a 404 is received
365      */
366     public void fetchFile(URL url, File outputPath)
367             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
368         fetchFile(url, outputPath, true);
369     }
370 
371     /**
372      * Retrieves a file from a given URL and saves it to the outputPath.
373      *
374      * @param url        the URL of the file to download
375      * @param outputPath the path to the save the file to
376      * @param useProxy   whether to use the configured proxy when downloading
377      *                   files
378      * @throws DownloadFailedException       is thrown if there is an error downloading the file
379      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
380      * @throws TooManyRequestsException      thrown when a 429 is received
381      * @throws ResourceNotFoundException     thrown when a 404 is received
382      */
383     public void fetchFile(URL url, File outputPath, boolean useProxy) throws DownloadFailedException,
384             TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
385         try {
386             if ("file".equals(url.getProtocol())) {
387                 final Path p = Paths.get(url.toURI());
388                 Files.copy(p, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING);
389             } else {
390                 final BasicClassicHttpRequest req;
391                 req = new BasicClassicHttpRequest(Method.GET, url.toURI());
392                 try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
393                     final SaveToFileResponseHandler responseHandler = new SaveToFileResponseHandler(outputPath);
394                     hc.execute(req, getPreEmptiveAuthContext(), responseHandler);
395                 }
396             }
397         } catch (HttpResponseException hre) {
398             wrapAndThrowHttpResponseException(url.toString(), hre);
399         } catch (SSLHandshakeException ex) {
400             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
401                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
402                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
403                 throw new URLConnectionFailureException(msg, ex);
404             }
405             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
406             throw new DownloadFailedException(msg, ex);
407         } catch (RuntimeException | URISyntaxException | IOException ex) {
408             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
409             throw new DownloadFailedException(msg, ex);
410         }
411     }
412 
413     private static void wrapAndThrowHttpResponseException(String url, HttpResponseException hre)
414             throws ResourceNotFoundException, TooManyRequestsException, DownloadFailedException {
415         final String messageFormat = "%s - Server status: %d - Server reason: %s";
416         switch (hre.getStatusCode()) {
417             case 404:
418                 throw new ResourceNotFoundException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
419             case 429:
420                 throw new TooManyRequestsException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
421             default:
422                 throw new DownloadFailedException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()), hre);
423         }
424     }
425 
426     /**
427      * Retrieves a file from a given URL using an ad-hoc created CredentialsProvider if needed
428      * and saves it to the outputPath.
429      *
430      * @param url         the URL of the file to download
431      * @param outputPath  the path to the save the file to
432      * @param useProxy    whether to use the configured proxy when downloading files
433      * @param userKey     The settings property key for a potentially configured configured Basic-auth username
434      * @param passwordKey The settings property key for a potentially configured configured Basic-auth password
435      * @param tokenKey    The settings property key for a potentially configured Bearer-auth token
436      * @throws DownloadFailedException       is thrown if there is an error downloading the file
437      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
438      * @throws TooManyRequestsException      thrown when a 429 is received
439      * @throws ResourceNotFoundException     thrown when a 404 is received
440      * @implNote This method should only be used in cases where the target host cannot be determined beforehand from settings, so that ad-hoc
441      * Credentials needs to be constructed for the target URL when the user/password keys point to configured credentials. The method delegates to
442      * {@link #fetchFile(URL, File, boolean)} when credentials are not configured for the given keys or the resource points to a file.
443      */
444     public void fetchFile(URL url, File outputPath, boolean useProxy, String userKey, String passwordKey, String tokenKey)
445             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
446         final boolean basicConfigured = userKey != null && settings.getString(userKey) != null
447                 && passwordKey != null && settings.getString(passwordKey) != null;
448         final boolean tokenConfigured = tokenKey != null && settings.getString(tokenKey) != null;
449         if ("file".equals(url.getProtocol()) || (!basicConfigured && !tokenConfigured)) {
450             // no credentials configured, so use the default fetchFile
451             fetchFile(url, outputPath, useProxy);
452             return;
453         }
454         final String theProtocol = url.getProtocol();
455         if (!("http".equals(theProtocol) || "https".equals(theProtocol))) {
456             throw new DownloadFailedException("Unsupported protocol in the URL; only file, http and https are supported");
457         }
458         try {
459             final HttpClientContext dedicatedAuthContext = HttpClientContext.create();
460             final CredentialsStore dedicatedCredentialStore = new SystemDefaultCredentialsProvider();
461             final AuthCache dedicatedAuthCache = new BasicAuthCache();
462             configureCredentials(url, url.toString(), userKey, passwordKey, tokenKey, dedicatedCredentialStore, dedicatedAuthCache);
463             if (useProxy && proxyAuthScope != null) {
464                 tryConfigureProxyCredentials(dedicatedCredentialStore, dedicatedAuthCache);
465             }
466             dedicatedAuthContext.setCredentialsProvider(dedicatedCredentialStore);
467             dedicatedAuthContext.setAuthCache(dedicatedAuthCache);
468             try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
469                 final BasicClassicHttpRequest req = new BasicClassicHttpRequest(Method.GET, url.toURI());
470                 final SaveToFileResponseHandler responseHandler = new SaveToFileResponseHandler(outputPath);
471                 hc.execute(req, dedicatedAuthContext, responseHandler);
472             }
473         } catch (HttpResponseException hre) {
474             wrapAndThrowHttpResponseException(url.toString(), hre);
475         } catch (SSLHandshakeException ex) {
476             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
477                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
478                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
479                 throw new URLConnectionFailureException(msg, ex);
480             }
481             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
482             throw new DownloadFailedException(msg, ex);
483         } catch (RuntimeException | URISyntaxException | IOException ex) {
484             final String msg = format("Download failed, unable to copy '%s' to '%s'; %s", url, outputPath.getAbsolutePath(), ex.getMessage());
485             throw new DownloadFailedException(msg, ex);
486         }
487     }
488 
489     /**
490      * Add the proxy credentials to the CredentialsProvider and AuthCache instances when proxy-authentication is configured in the settings.
491      * @param credentialsProvider The credentialStore to configure the credentials in
492      * @param authCache The AuthCache to cache the pre-empted credentials in
493      */
494     private void tryConfigureProxyCredentials(@NotNull CredentialsStore credentialsProvider, @NotNull AuthCache authCache) {
495         if (proxyPreEmptAuth != null) {
496             credentialsProvider.setCredentials(proxyAuthScope, proxyCreds);
497             authCache.put(proxyHttpHost, proxyPreEmptAuth);
498         }
499     }
500 
501     /**
502      * Posts a payload to the URL and returns the response as a string.
503      *
504      * @param url         the URL to POST to
505      * @param payload     the Payload to post
506      * @param payloadType the string describing the payload's mime-type
507      * @param hdr         Additional headers to add to the HTTP request
508      * @return the content of the response
509      * @throws DownloadFailedException       is thrown if there is an error downloading the file
510      * @throws URLConnectionFailureException is thrown when certificate-chain trust errors occur downloading the file
511      * @throws TooManyRequestsException      thrown when a 429 is received
512      * @throws ResourceNotFoundException     thrown when a 404 is received
513      */
514     public String postBasedFetchContent(URI url, String payload, ContentType payloadType, List<Header> hdr)
515             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException, URLConnectionFailureException {
516         try {
517             if (url.getScheme() == null || !url.getScheme().toLowerCase(Locale.ROOT).matches("^https?")) {
518                 throw new IllegalArgumentException("Unsupported protocol in the URL; only http and https are supported");
519             } else {
520                 final BasicClassicHttpRequest req;
521                 req = new BasicClassicHttpRequest(Method.POST, url);
522                 req.setEntity(new StringEntity(payload, payloadType));
523                 for (Header h : hdr) {
524                     req.addHeader(h);
525                 }
526                 final String result;
527                 try (CloseableHttpClient hc = httpClientBuilder.build()) {
528                     result = hc.execute(req, getPreEmptiveAuthContext(), new BasicHttpClientResponseHandler());
529                 }
530                 return result;
531             }
532         } catch (HttpResponseException hre) {
533             wrapAndThrowHttpResponseException(url.toString(), hre);
534             throw new InternalError("wrapAndThrowHttpResponseException will always throw an exception but Java compiler fails to spot it");
535         } catch (SSLHandshakeException ex) {
536             if (ex.getMessage().contains("unable to find valid certification path to requested target")) {
537                 final String msg = String.format("Unable to connect to '%s' - the Java trust store does not contain a trusted root for the cert. "
538                         + "Please see https://github.com/jeremylong/InstallCert for one method of updating the trusted certificates.", url);
539                 throw new URLConnectionFailureException(msg, ex);
540             }
541             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
542             throw new DownloadFailedException(msg, ex);
543         } catch (IOException | RuntimeException ex) {
544             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
545             throw new DownloadFailedException(msg, ex);
546         }
547     }
548 
549     /**
550      * Retrieves a file from a given URL and returns the contents.
551      *
552      * @param url     the URL of the file to download
553      * @param charset The characterset to use to interpret the binary content of the file
554      * @return the content of the file
555      * @throws DownloadFailedException   is thrown if there is an error
556      *                                   downloading the file
557      * @throws TooManyRequestsException  thrown when a 429 is received
558      * @throws ResourceNotFoundException thrown when a 404 is received
559      */
560     public String fetchContent(URL url, Charset charset) throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException {
561         return fetchContent(url, true, charset);
562     }
563 
564     /**
565      * Retrieves a file from a given URL and returns the contents.
566      *
567      * @param url      the URL of the file to download
568      * @param useProxy whether to use the configured proxy when downloading
569      *                 files
570      * @param charset  The characterset to use to interpret the binary content of the file
571      * @return the content of the file
572      * @throws DownloadFailedException   is thrown if there is an error
573      *                                   downloading the file
574      * @throws TooManyRequestsException  thrown when a 429 is received
575      * @throws ResourceNotFoundException thrown when a 404 is received
576      */
577     public String fetchContent(URL url, boolean useProxy, Charset charset)
578             throws DownloadFailedException, TooManyRequestsException, ResourceNotFoundException {
579         try {
580             final String result;
581             if ("file".equals(url.getProtocol())) {
582                 final Path p = Paths.get(url.toURI());
583                 result = Files.readString(p, charset);
584             } else {
585                 final BasicClassicHttpRequest req;
586                 req = new BasicClassicHttpRequest(Method.GET, url.toURI());
587                 try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
588                     req.addHeader(HttpHeaders.ACCEPT_CHARSET, charset.name());
589                     final ExplicitCharsetToStringResponseHandler responseHandler = new ExplicitCharsetToStringResponseHandler(charset);
590                     result = hc.execute(req, getPreEmptiveAuthContext(), responseHandler);
591                 }
592             }
593             return result;
594         } catch (HttpResponseException hre) {
595             wrapAndThrowHttpResponseException(url.toString(), hre);
596             throw new InternalError("wrapAndThrowHttpResponseException will always throw an exception but Java compiler fails to spot it");
597         } catch (RuntimeException | URISyntaxException | IOException ex) {
598             final String msg = format("Download failed, error downloading '%s'; %s", url, ex.getMessage());
599             throw new DownloadFailedException(msg, ex);
600         }
601     }
602 
603     /**
604      * Gets a HttpClientContext that supports pre-emptive authentication.
605      * @return A HttpClientContext pre-configured with the authentication cache build from the settings.
606      */
607     public HttpClientContext getPreEmptiveAuthContext() {
608         final HttpClientContext context = HttpClientContext.create();
609         context.setCredentialsProvider(credentialsProvider);
610         context.setAuthCache(authCache);
611         return context;
612     }
613 
614     /**
615      * Gets a pre-configured HttpClient.
616      * Mainly targeted for use in paged resultset scenarios with multiple roundtrips.
617      * @param useProxy Whether to use the configuration that includes proxy-settings
618      * @return A HttpClient pre-configured with the settings.
619      */
620     public CloseableHttpClient getHttpClient(boolean useProxy) {
621         return useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build();
622     }
623 
624     /**
625      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
626      *
627      * @param url             The url of the resource
628      * @param handler   The responsehandler to handle the response
629      * @param <T>             The return-type for the responseHandler
630      * @return The response handler result
631      * @throws IOException               on I/O Exceptions
632      * @throws TooManyRequestsException  When HTTP status 429 is encountered
633      * @throws ResourceNotFoundException When HTTP status 404 is encountered
634      */
635     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler)
636             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
637         return fetchAndHandle(url, handler, Collections.emptyList(), true);
638     }
639 
640     /**
641      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
642      *
643      * @param url               The url of the resource
644      * @param handler   The responsehandler to handle the response
645      * @param hdr Additional headers to add to the HTTP request
646      * @param <T>               The return-type for the responseHandler
647      * @return The response handler result
648      * @throws IOException               on I/O Exceptions
649      * @throws TooManyRequestsException  When HTTP status 429 is encountered
650      * @throws ResourceNotFoundException When HTTP status 404 is encountered
651      */
652     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler, @NotNull List<Header> hdr)
653             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
654         return fetchAndHandle(url, handler, hdr, true);
655     }
656 
657     /**
658      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
659      *
660      * @param url               The url of the resource
661      * @param handler   The responsehandler to handle the response
662      * @param hdr Additional headers to add to the HTTP request
663      * @param useProxy          Whether to use the configured proxy for the connection
664      * @param <T>               The return-type for the responseHandler
665      * @return The response handler result
666      * @throws IOException               on I/O Exceptions
667      * @throws TooManyRequestsException  When HTTP status 429 is encountered
668      * @throws ResourceNotFoundException When HTTP status 404 is encountered
669      */
670     public <T> T fetchAndHandle(@NotNull URL url, @NotNull HttpClientResponseHandler<T> handler, @NotNull List<Header> hdr, boolean useProxy)
671             throws IOException, TooManyRequestsException, ResourceNotFoundException, URISyntaxException {
672         final T data;
673         if ("file".equals(url.getProtocol())) {
674             final Path p = Paths.get(url.toURI());
675             try (InputStream is = Files.newInputStream(p)) {
676                 final HttpEntity dummyEntity = new BasicHttpEntity(is, ContentType.APPLICATION_JSON);
677                 final ClassicHttpResponse dummyResponse = new BasicClassicHttpResponse(200);
678                 dummyResponse.setEntity(dummyEntity);
679                 data = handler.handleResponse(dummyResponse);
680             } catch (HttpException e) {
681                 throw new IllegalStateException("HttpException encountered emulating a HTTP response from a file", e);
682             }
683         } else {
684             try (CloseableHttpClient hc = useProxy ? httpClientBuilder.build() : httpClientBuilderExplicitNoproxy.build()) {
685                 return fetchAndHandle(hc, url, handler, hdr);
686             }
687         }
688         return data;
689     }
690     /**
691      * Download a resource from the given URL and have its content handled by the given ResponseHandler.
692      *
693      * @param client            The HTTP Client to reuse for the request
694      * @param url               The url of the resource
695      * @param handler   The responsehandler to handle the response
696      * @param hdr Additional headers to add to the HTTP request
697      * @param <T>               The return-type for the responseHandler
698      * @return The response handler result
699      * @throws IOException               on I/O Exceptions
700      * @throws TooManyRequestsException  When HTTP status 429 is encountered
701      * @throws ResourceNotFoundException When HTTP status 404 is encountered
702      */
703     public <T> T fetchAndHandle(@NotNull CloseableHttpClient client, @NotNull URL url, @NotNull HttpClientResponseHandler<T> handler,
704                                 @NotNull List<Header> hdr) throws IOException, TooManyRequestsException, ResourceNotFoundException {
705         try {
706             final String theProtocol = url.getProtocol();
707             if (!("http".equals(theProtocol) || "https".equals(theProtocol))) {
708                 throw new DownloadFailedException("Unsupported protocol in the URL; only http and https are supported");
709             }
710             final BasicClassicHttpRequest req = new BasicClassicHttpRequest(Method.GET, url.toURI());
711             for (Header h : hdr) {
712                 req.addHeader(h);
713             }
714             final HttpClientContext context = getPreEmptiveAuthContext();
715             return client.execute(req, context, handler);
716         } catch (HttpResponseException hre) {
717             final String messageFormat = "%s - Server status: %d - Server reason: %s";
718             switch (hre.getStatusCode()) {
719                 case 404:
720                     throw new ResourceNotFoundException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
721                 case 429:
722                     throw new TooManyRequestsException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
723                 default:
724                     throw new IOException(String.format(messageFormat, url, hre.getStatusCode(), hre.getReasonPhrase()));
725             }
726         } catch (RuntimeException | URISyntaxException ex) {
727             final String msg = format("Download failed, unable to retrieve and parse '%s'; %s", url, ex.getMessage());
728             throw new IOException(msg, ex);
729         }
730     }
731 
732     private static class SelectiveProxySelector extends ProxySelector {
733 
734         /**
735          * The suffix-match entries from the nonProxyHosts (those starting with a {@code *}).
736          */
737         private final List<String> suffixMatch = new ArrayList<>();
738         /**
739          * The full host entries from the nonProxyHosts (those <em>not</em> starting with a {@code *}).
740          */
741         private final List<String> fullmatch = new ArrayList<>();
742         /**
743          * The proxy use when no proxy-exception is found.
744          */
745         private final Proxy configuredProxy;
746 
747         SelectiveProxySelector(Proxy httpHost, String[] nonProxyHostsPatterns) {
748             for (String nonProxyHostPattern : nonProxyHostsPatterns) {
749                 if (nonProxyHostPattern.startsWith("*")) {
750                     suffixMatch.add(nonProxyHostPattern.substring(1));
751                 } else {
752                     fullmatch.add(nonProxyHostPattern);
753                 }
754             }
755             this.configuredProxy = httpHost;
756         }
757 
758         @Override
759         public List<Proxy> select(URI uri) {
760             final String theHost = uri.getHost();
761             if (fullmatch.contains(theHost)) {
762                 return Collections.singletonList(Proxy.NO_PROXY);
763             } else {
764                 for (String suffix : suffixMatch) {
765                     if (theHost.endsWith(suffix)) {
766                         return Collections.singletonList(Proxy.NO_PROXY);
767                     }
768                 }
769             }
770             return List.of(configuredProxy);
771         }
772 
773         @Override
774         public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
775             // nothing to be done for this single proxy proxy-selector
776         }
777     }
778 }