View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
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    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit;
16  
17  import java.io.ByteArrayInputStream;
18  import java.io.ByteArrayOutputStream;
19  import java.io.EOFException;
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.net.InetAddress;
25  import java.net.URI;
26  import java.net.URISyntaxException;
27  import java.net.URL;
28  import java.nio.charset.Charset;
29  import java.nio.file.Files;
30  import java.util.ArrayList;
31  import java.util.HashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.WeakHashMap;
35  import java.util.concurrent.TimeUnit;
36  
37  import javax.net.ssl.HostnameVerifier;
38  import javax.net.ssl.SSLContext;
39  import javax.net.ssl.SSLPeerUnverifiedException;
40  import javax.net.ssl.SSLSocketFactory;
41  
42  import org.apache.commons.io.IOUtils;
43  import org.apache.commons.lang3.StringUtils;
44  import org.apache.commons.lang3.reflect.FieldUtils;
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  import org.apache.http.ConnectionClosedException;
48  import org.apache.http.Header;
49  import org.apache.http.HttpEntity;
50  import org.apache.http.HttpEntityEnclosingRequest;
51  import org.apache.http.HttpException;
52  import org.apache.http.HttpHost;
53  import org.apache.http.HttpRequest;
54  import org.apache.http.HttpRequestInterceptor;
55  import org.apache.http.HttpResponse;
56  import org.apache.http.auth.AuthScheme;
57  import org.apache.http.auth.AuthScope;
58  import org.apache.http.auth.Credentials;
59  import org.apache.http.client.AuthCache;
60  import org.apache.http.client.CredentialsProvider;
61  import org.apache.http.client.config.RequestConfig;
62  import org.apache.http.client.methods.CloseableHttpResponse;
63  import org.apache.http.client.methods.HttpGet;
64  import org.apache.http.client.methods.HttpHead;
65  import org.apache.http.client.methods.HttpPatch;
66  import org.apache.http.client.methods.HttpPost;
67  import org.apache.http.client.methods.HttpPut;
68  import org.apache.http.client.methods.HttpRequestBase;
69  import org.apache.http.client.methods.HttpTrace;
70  import org.apache.http.client.methods.HttpUriRequest;
71  import org.apache.http.client.protocol.HttpClientContext;
72  import org.apache.http.client.protocol.RequestAcceptEncoding;
73  import org.apache.http.client.protocol.RequestAddCookies;
74  import org.apache.http.client.protocol.RequestAuthCache;
75  import org.apache.http.client.protocol.RequestDefaultHeaders;
76  import org.apache.http.client.protocol.RequestExpectContinue;
77  import org.apache.http.client.protocol.ResponseProcessCookies;
78  import org.apache.http.client.utils.URLEncodedUtils;
79  import org.apache.http.config.ConnectionConfig;
80  import org.apache.http.config.RegistryBuilder;
81  import org.apache.http.config.SocketConfig;
82  import org.apache.http.conn.DnsResolver;
83  import org.apache.http.conn.routing.RouteInfo;
84  import org.apache.http.conn.socket.ConnectionSocketFactory;
85  import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
86  import org.apache.http.conn.ssl.DefaultHostnameVerifier;
87  import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
88  import org.apache.http.conn.util.PublicSuffixMatcher;
89  import org.apache.http.conn.util.PublicSuffixMatcherLoader;
90  import org.apache.http.cookie.CookieSpecProvider;
91  import org.apache.http.entity.ContentType;
92  import org.apache.http.entity.StringEntity;
93  import org.apache.http.entity.mime.MultipartEntityBuilder;
94  import org.apache.http.entity.mime.content.InputStreamBody;
95  import org.apache.http.impl.client.BasicAuthCache;
96  import org.apache.http.impl.client.CloseableHttpClient;
97  import org.apache.http.impl.client.HttpClientBuilder;
98  import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
99  import org.apache.http.protocol.HttpContext;
100 import org.apache.http.protocol.HttpProcessorBuilder;
101 import org.apache.http.protocol.RequestContent;
102 import org.apache.http.protocol.RequestTargetHost;
103 import org.apache.http.ssl.SSLContexts;
104 import org.apache.http.util.TextUtils;
105 import org.htmlunit.WebRequest.HttpHint;
106 import org.htmlunit.http.HttpUtils;
107 import org.htmlunit.httpclient.HtmlUnitCookieSpecProvider;
108 import org.htmlunit.httpclient.HtmlUnitCookieStore;
109 import org.htmlunit.httpclient.HtmlUnitRedirectStrategie;
110 import org.htmlunit.httpclient.HtmlUnitSSLConnectionSocketFactory;
111 import org.htmlunit.httpclient.SocksConnectionSocketFactory;
112 import org.htmlunit.util.KeyDataPair;
113 import org.htmlunit.util.MimeType;
114 import org.htmlunit.util.NameValuePair;
115 import org.htmlunit.util.UrlUtils;
116 
117 /**
118  * Default implementation of {@link WebConnection}, using the HttpClient library to perform HTTP requests.
119  *
120  * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
121  * @author Noboru Sinohara
122  * @author David D. Kilzer
123  * @author Marc Guillemot
124  * @author Brad Clarke
125  * @author Ahmed Ashour
126  * @author Nicolas Belisle
127  * @author Ronald Brill
128  * @author John J Murdoch
129  * @author Carsten Steul
130  * @author Hartmut Arlt
131  * @author Lai Quang Duong
132  */
133 public class HttpWebConnection implements WebConnection {
134 
135     private static final Log LOG = LogFactory.getLog(HttpWebConnection.class);
136 
137     private static final String HACKED_COOKIE_POLICY = "mine";
138 
139     // have one per thread because this is (re)configured for every call (see configureHttpProcessorBuilder)
140     // do not use a ThreadLocal because this in only accessed form this class
141     private final Map<Thread, HttpClientBuilder> httpClientBuilder_ = new WeakHashMap<>();
142     private final WebClient webClient_;
143 
144     private String virtualHost_;
145     private final HtmlUnitCookieSpecProvider htmlUnitCookieSpecProvider_;
146     private final WebClientOptions usedOptions_;
147     private PoolingHttpClientConnectionManager connectionManager_;
148 
149     /** Authentication cache shared among all threads of a web client. */
150     private final AuthCache sharedAuthCache_ = new SynchronizedAuthCache();
151 
152     /** Maintains a separate {@link HttpClientContext} object per HttpWebConnection and thread. */
153     private final Map<Thread, HttpClientContext> httpClientContextByThread_ = new WeakHashMap<>();
154 
155     /**
156      * Creates a new HTTP web connection instance.
157      * @param webClient the WebClient that is using this connection
158      */
159     public HttpWebConnection(final WebClient webClient) {
160         super();
161         webClient_ = webClient;
162         htmlUnitCookieSpecProvider_ = new HtmlUnitCookieSpecProvider(webClient.getBrowserVersion());
163         usedOptions_ = new WebClientOptions();
164     }
165 
166     /**
167      * {@inheritDoc}
168      */
169     @Override
170     public WebResponse getResponse(final WebRequest webRequest) throws IOException {
171         final HttpClientBuilder builder = reconfigureHttpClientIfNeeded(getHttpClientBuilder(), webRequest);
172 
173         HttpUriRequest httpMethod = null;
174         try {
175             try {
176                 httpMethod = makeHttpMethod(webRequest, builder);
177             }
178             catch (final URISyntaxException e) {
179                 throw new IOException("Unable to create URI from URL: " + webRequest.getUrl().toExternalForm()
180                         + " (reason: " + e.getMessage() + ")", e);
181             }
182 
183             final URL url = webRequest.getUrl();
184             final HttpHost httpHost = new HttpHost(url.getHost(), url.getPort(), url.getProtocol());
185             final long startTime = System.currentTimeMillis();
186 
187             final HttpContext httpContext = getHttpContext();
188             try {
189                 try (CloseableHttpClient closeableHttpClient = builder.build()) {
190                     try (CloseableHttpResponse httpResponse =
191                             closeableHttpClient.execute(httpHost, httpMethod, httpContext)) {
192                         return downloadResponse(httpMethod, webRequest, httpResponse, startTime);
193                     }
194                 }
195             }
196             catch (final SSLPeerUnverifiedException ex) {
197                 // Try to use only SSLv3 instead
198                 if (webClient_.getOptions().isUseInsecureSSL()) {
199                     HtmlUnitSSLConnectionSocketFactory.setUseSSL3Only(httpContext, true);
200                     try (CloseableHttpClient closeableHttpClient = builder.build()) {
201                         try (CloseableHttpResponse httpResponse =
202                                 closeableHttpClient.execute(httpHost, httpMethod, httpContext)) {
203                             return downloadResponse(httpMethod, webRequest, httpResponse, startTime);
204                         }
205                     }
206                 }
207                 throw ex;
208             }
209             catch (final Error e) {
210                 // in case a StackOverflowError occurs while the connection is leased, it won't get released.
211                 // Calling code may catch the StackOverflowError, but due to the leak, the httpClient_ may
212                 // come out of connections and throw a ConnectionPoolTimeoutException.
213                 // => best solution, discard the HttpClient instance.
214                 httpClientBuilder_.remove(Thread.currentThread());
215                 throw e;
216             }
217         }
218         finally {
219             if (httpMethod != null) {
220                 onResponseGenerated(httpMethod);
221             }
222         }
223     }
224 
225     /**
226      * Called when the response has been generated. Default action is to release
227      * the HttpMethod's connection. Subclasses may override.
228      * @param httpMethod the httpMethod used (can be null)
229      */
230     protected void onResponseGenerated(final HttpUriRequest httpMethod) {
231         // nothing to do
232     }
233 
234     /**
235      * Returns the {@link HttpClientContext} for the current thread. Creates a new one if necessary.
236      */
237     private synchronized HttpContext getHttpContext() {
238         HttpClientContext httpClientContext = httpClientContextByThread_.get(Thread.currentThread());
239         if (httpClientContext == null) {
240             httpClientContext = new HttpClientContext();
241 
242             // set the shared authentication cache
243             httpClientContext.setAttribute(HttpClientContext.AUTH_CACHE, sharedAuthCache_);
244 
245             httpClientContextByThread_.put(Thread.currentThread(), httpClientContext);
246         }
247         return httpClientContext;
248     }
249 
250     private void setProxy(final HttpRequestBase httpRequest, final WebRequest webRequest) {
251         final InetAddress localAddress = webClient_.getOptions().getLocalAddress();
252         final RequestConfig.Builder requestBuilder = createRequestConfigBuilder(getTimeout(webRequest), localAddress);
253 
254         if (webRequest.getProxyHost() == null) {
255             requestBuilder.setProxy(null);
256             httpRequest.setConfig(requestBuilder.build());
257             return;
258         }
259 
260         final HttpHost proxy = new HttpHost(webRequest.getProxyHost(),
261                                     webRequest.getProxyPort(), webRequest.getProxyScheme());
262         if (webRequest.isSocksProxy()) {
263             SocksConnectionSocketFactory.setSocksProxy(getHttpContext(), proxy);
264         }
265         else {
266             requestBuilder.setProxy(proxy);
267             httpRequest.setConfig(requestBuilder.build());
268         }
269     }
270 
271     /**
272      * Creates an <code>HttpMethod</code> instance according to the specified parameters.
273      * @param webRequest the request
274      * @param httpClientBuilder the httpClientBuilder that will be configured
275      * @return the <code>HttpMethod</code> instance constructed according to the specified parameters
276      * @throws URISyntaxException in case of syntax problems
277      */
278     private HttpUriRequest makeHttpMethod(final WebRequest webRequest, final HttpClientBuilder httpClientBuilder)
279         throws URISyntaxException {
280 
281         final HttpContext httpContext = getHttpContext();
282         final Charset charset = webRequest.getCharset();
283         // Make sure that the URL is fully encoded. IE actually sends some Unicode chars in request
284         // URLs; because of this we allow some Unicode chars in URLs. However, at this point we're
285         // handing things over the HttpClient, and HttpClient will blow up if we leave these Unicode
286         // chars in the URL.
287         final URL url = UrlUtils.encodeUrl(webRequest.getUrl(), charset);
288 
289         URI uri = UrlUtils.toURI(url, escapeQuery(url.getQuery()));
290         if (getVirtualHost() != null) {
291             uri = URI.create(getVirtualHost());
292         }
293         final HttpRequestBase httpMethod = buildHttpMethod(webRequest.getHttpMethod(), uri);
294         setProxy(httpMethod, webRequest);
295 
296         // developer note:
297         // this has to be in sync with org.htmlunit.WebRequest.getRequestParameters()
298 
299         // POST, PUT, PATCH, DELETE, OPTIONS
300         if ((httpMethod instanceof HttpEntityEnclosingRequest)
301                 && (httpMethod instanceof HttpPost
302                         || httpMethod instanceof HttpPut
303                         || httpMethod instanceof HttpPatch
304                         || httpMethod instanceof org.htmlunit.httpclient.HttpDelete
305                         || httpMethod instanceof org.htmlunit.httpclient.HttpOptions)) {
306 
307             final HttpEntityEnclosingRequest method = (HttpEntityEnclosingRequest) httpMethod;
308 
309             if (FormEncodingType.URL_ENCODED == webRequest.getEncodingType()) {
310                 if (webRequest.getRequestBody() == null) {
311                     final List<NameValuePair> pairs = webRequest.getRequestParameters();
312                     final String query = HttpUtils.toQueryFormFields(pairs, charset);
313 
314                     final StringEntity urlEncodedEntity;
315                     if (webRequest.hasHint(HttpHint.IncludeCharsetInContentTypeHeader)) {
316                         urlEncodedEntity = new StringEntity(query,
317                                 ContentType.create(URLEncodedUtils.CONTENT_TYPE, charset));
318 
319                     }
320                     else {
321                         urlEncodedEntity = new StringEntity(query, charset);
322                         urlEncodedEntity.setContentType(URLEncodedUtils.CONTENT_TYPE);
323                     }
324                     method.setEntity(urlEncodedEntity);
325                 }
326                 else {
327                     final String body = StringUtils.defaultString(webRequest.getRequestBody());
328                     final StringEntity urlEncodedEntity = new StringEntity(body, charset);
329                     urlEncodedEntity.setContentType(URLEncodedUtils.CONTENT_TYPE);
330                     method.setEntity(urlEncodedEntity);
331                 }
332             }
333             else if (FormEncodingType.TEXT_PLAIN == webRequest.getEncodingType()) {
334                 if (webRequest.getRequestBody() == null) {
335                     final StringBuilder body = new StringBuilder();
336                     for (final NameValuePair pair : webRequest.getRequestParameters()) {
337                         body.append(StringUtils.remove(StringUtils.remove(pair.getName(), '\r'), '\n'))
338                             .append('=')
339                             .append(StringUtils.remove(StringUtils.remove(pair.getValue(), '\r'), '\n'))
340                             .append("\r\n");
341                     }
342                     final StringEntity bodyEntity = new StringEntity(body.toString(), charset);
343                     bodyEntity.setContentType(MimeType.TEXT_PLAIN);
344                     method.setEntity(bodyEntity);
345                 }
346                 else {
347                     final String body = StringUtils.defaultString(webRequest.getRequestBody());
348                     final StringEntity bodyEntity =
349                             new StringEntity(body, ContentType.create(MimeType.TEXT_PLAIN, charset));
350                     method.setEntity(bodyEntity);
351                 }
352             }
353             else if (FormEncodingType.MULTIPART == webRequest.getEncodingType()) {
354                 final Charset c = getCharset(charset, webRequest.getRequestParameters());
355                 final MultipartEntityBuilder builder = MultipartEntityBuilder.create().setLaxMode();
356                 builder.setCharset(c);
357 
358                 for (final NameValuePair pair : webRequest.getRequestParameters()) {
359                     if (pair instanceof KeyDataPair) {
360                         buildFilePart((KeyDataPair) pair, builder);
361                     }
362                     else {
363                         builder.addTextBody(pair.getName(), pair.getValue(),
364                                 ContentType.create(MimeType.TEXT_PLAIN, charset));
365                     }
366                 }
367                 method.setEntity(builder.build());
368             }
369             else {
370                 // for instance a PATCH request
371                 final String body = webRequest.getRequestBody();
372                 if (body != null) {
373                     method.setEntity(new StringEntity(body, charset));
374                 }
375             }
376         }
377         else {
378             // GET, TRACE, HEAD
379             final List<NameValuePair> pairs = webRequest.getRequestParameters();
380             if (!pairs.isEmpty()) {
381                 final String query = HttpUtils.toQueryFormFields(pairs, charset);
382                 uri = UrlUtils.toURI(url, query);
383                 httpMethod.setURI(uri);
384             }
385         }
386 
387         configureHttpProcessorBuilder(httpClientBuilder, webRequest);
388 
389         // Tell the client where to get its credentials from
390         // (it may have changed on the webClient since last call to getHttpClientFor(...))
391         final CredentialsProvider credentialsProvider = webClient_.getCredentialsProvider();
392 
393         // if the used url contains credentials, we have to add this
394         final Credentials requestUrlCredentials = webRequest.getUrlCredentials();
395         if (null != requestUrlCredentials) {
396             final URL requestUrl = webRequest.getUrl();
397             final AuthScope authScope = new AuthScope(requestUrl.getHost(), requestUrl.getPort());
398             // updating our client to keep the credentials for the next request
399             credentialsProvider.setCredentials(authScope, requestUrlCredentials);
400         }
401 
402         // if someone has set credentials to this request, we have to add this
403         final Credentials requestCredentials = webRequest.getCredentials();
404         if (null != requestCredentials) {
405             final URL requestUrl = webRequest.getUrl();
406             final AuthScope authScope = new AuthScope(requestUrl.getHost(), requestUrl.getPort());
407             // updating our client to keep the credentials for the next request
408             credentialsProvider.setCredentials(authScope, requestCredentials);
409         }
410         httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
411         httpContext.removeAttribute(HttpClientContext.CREDS_PROVIDER);
412         httpContext.removeAttribute(HttpClientContext.TARGET_AUTH_STATE);
413         return httpMethod;
414     }
415 
416     private static String escapeQuery(final String query) {
417         if (query == null) {
418             return null;
419         }
420         return query.replace("%%", "%25%25");
421     }
422 
423     private static Charset getCharset(final Charset charset, final List<NameValuePair> pairs) {
424         for (final NameValuePair pair : pairs) {
425             if (pair instanceof KeyDataPair) {
426                 final KeyDataPair pairWithFile = (KeyDataPair) pair;
427                 if (pairWithFile.getData() == null && pairWithFile.getFile() != null) {
428                     final String fileName = pairWithFile.getFile().getName();
429                     final int length = fileName.length();
430                     for (int i = 0; i < length; i++) {
431                         if (fileName.codePointAt(i) > 127) {
432                             return charset;
433                         }
434                     }
435                 }
436             }
437         }
438         return null;
439     }
440 
441     void buildFilePart(final KeyDataPair pairWithFile, final MultipartEntityBuilder builder) {
442         String mimeType = pairWithFile.getMimeType();
443         if (mimeType == null) {
444             mimeType = MimeType.APPLICATION_OCTET_STREAM;
445         }
446 
447         final ContentType contentType = ContentType.create(mimeType);
448 
449         final File file = pairWithFile.getFile();
450         if (file != null) {
451             String filename = pairWithFile.getFileName();
452             if (filename == null) {
453                 filename = pairWithFile.getFile().getName();
454             }
455             builder.addBinaryBody(pairWithFile.getName(), file, contentType, filename);
456             return;
457         }
458 
459         final byte[] data = pairWithFile.getData();
460         if (data != null) {
461             String filename = pairWithFile.getFileName();
462             if (filename == null) {
463                 filename = pairWithFile.getValue();
464             }
465 
466             builder.addBinaryBody(pairWithFile.getName(), new ByteArrayInputStream(data),
467                     contentType, filename);
468             return;
469         }
470 
471         builder.addPart(pairWithFile.getName(),
472                 // Overridden in order not to have a chunked response.
473                 new InputStreamBody(new ByteArrayInputStream(new byte[0]), contentType, pairWithFile.getValue()) {
474                 @Override
475                 public long getContentLength() {
476                     return 0;
477                 }
478             });
479     }
480 
481     /**
482      * Creates and returns a new HttpClient HTTP method based on the specified parameters.
483      * @param submitMethod the submit method being used
484      * @param uri the uri being used
485      * @return a new HttpClient HTTP method based on the specified parameters
486      */
487     private static HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri) {
488         final HttpRequestBase method;
489         switch (submitMethod) {
490             case GET:
491                 method = new HttpGet(uri);
492                 break;
493 
494             case POST:
495                 method = new HttpPost(uri);
496                 break;
497 
498             case PUT:
499                 method = new HttpPut(uri);
500                 break;
501 
502             case DELETE:
503                 method = new org.htmlunit.httpclient.HttpDelete(uri);
504                 break;
505 
506             case OPTIONS:
507                 method = new org.htmlunit.httpclient.HttpOptions(uri);
508                 break;
509 
510             case HEAD:
511                 method = new HttpHead(uri);
512                 break;
513 
514             case TRACE:
515                 method = new HttpTrace(uri);
516                 break;
517 
518             case PATCH:
519                 method = new HttpPatch(uri);
520                 break;
521 
522             default:
523                 throw new IllegalStateException("Submit method not yet supported: " + submitMethod);
524         }
525         return method;
526     }
527 
528     /**
529      * Lazily initializes the internal HTTP client.
530      *
531      * @return the initialized HTTP client
532      */
533     protected HttpClientBuilder getHttpClientBuilder() {
534         final Thread currentThread = Thread.currentThread();
535         HttpClientBuilder builder = httpClientBuilder_.get(currentThread);
536         if (builder == null) {
537             builder = createHttpClientBuilder();
538 
539             // this factory is required later
540             // to be sure this is done, we do it outside the createHttpClient() call
541             final RegistryBuilder<CookieSpecProvider> registeryBuilder
542                 = RegistryBuilder.<CookieSpecProvider>create()
543                             .register(HACKED_COOKIE_POLICY, htmlUnitCookieSpecProvider_);
544             builder.setDefaultCookieSpecRegistry(registeryBuilder.build());
545 
546             builder.setDefaultCookieStore(new HtmlUnitCookieStore(webClient_.getCookieManager()));
547             builder.setUserAgent(webClient_.getBrowserVersion().getUserAgent());
548             httpClientBuilder_.put(currentThread, builder);
549         }
550 
551         return builder;
552     }
553 
554     /**
555      * Returns the timeout to use for socket and connection timeouts for HttpConnectionManager.
556      * Is overridden to 0 by StreamingWebConnection which keeps reading after a timeout and
557      * must have long running connections explicitly terminated.
558      * @param webRequest the request might have his own timeout
559      * @return the WebClient's timeout
560      */
561     protected int getTimeout(final WebRequest webRequest) {
562         if (webRequest == null || webRequest.getTimeout() < 0) {
563             return webClient_.getOptions().getTimeout();
564         }
565 
566         return webRequest.getTimeout();
567     }
568 
569     /**
570      * Creates the <code>HttpClientBuilder</code> that will be used by this WebClient.
571      * Extensions may override this method in order to create a customized
572      * <code>HttpClientBuilder</code> instance (e.g. with a custom
573      * {@link org.apache.http.conn.ClientConnectionManager} to perform
574      * some tracking; see feature request 1438216).
575      * @return the <code>HttpClientBuilder</code> that will be used by this WebConnection
576      */
577     protected HttpClientBuilder createHttpClientBuilder() {
578         final HttpClientBuilder builder = HttpClientBuilder.create();
579         builder.setRedirectStrategy(new HtmlUnitRedirectStrategie());
580         configureTimeout(builder, getTimeout(null));
581         configureHttpsScheme(builder);
582         builder.setMaxConnPerRoute(6);
583 
584         builder.setConnectionManagerShared(true);
585         return builder;
586     }
587 
588     private void configureTimeout(final HttpClientBuilder builder, final int timeout) {
589         final InetAddress localAddress = webClient_.getOptions().getLocalAddress();
590         final RequestConfig.Builder requestBuilder = createRequestConfigBuilder(timeout, localAddress);
591         builder.setDefaultRequestConfig(requestBuilder.build());
592 
593         builder.setDefaultSocketConfig(createSocketConfigBuilder(timeout).build());
594 
595         getHttpContext().removeAttribute(HttpClientContext.REQUEST_CONFIG);
596         usedOptions_.setTimeout(timeout);
597     }
598 
599     private static RequestConfig.Builder createRequestConfigBuilder(final int timeout, final InetAddress localAddress) {
600         return RequestConfig.custom()
601                 .setCookieSpec(HACKED_COOKIE_POLICY)
602                 .setRedirectsEnabled(false)
603                 .setLocalAddress(localAddress)
604 
605                 // timeout
606                 .setConnectTimeout(timeout)
607                 .setConnectionRequestTimeout(timeout)
608                 .setSocketTimeout(timeout);
609     }
610 
611     private static SocketConfig.Builder createSocketConfigBuilder(final int timeout) {
612         return SocketConfig.custom()
613                 // timeout
614                 .setSoTimeout(timeout);
615     }
616 
617     /**
618      * React on changes that may have occurred on the WebClient settings.
619      * Registering as a listener would be probably better.
620      */
621     private HttpClientBuilder reconfigureHttpClientIfNeeded(final HttpClientBuilder httpClientBuilder,
622             final WebRequest webRequest) {
623         final WebClientOptions options = webClient_.getOptions();
624 
625         // register new SSL factory only if settings have changed
626         if (options.isUseInsecureSSL() != usedOptions_.isUseInsecureSSL()
627                 || options.getSSLClientCertificateStore() != usedOptions_.getSSLClientCertificateStore()
628                 || options.getSSLTrustStore() != usedOptions_.getSSLTrustStore()
629                 || options.getSSLClientCipherSuites() != usedOptions_.getSSLClientCipherSuites()
630                 || options.getSSLClientProtocols() != usedOptions_.getSSLClientProtocols()
631                 || options.getProxyConfig() != usedOptions_.getProxyConfig()) {
632             configureHttpsScheme(httpClientBuilder);
633 
634             if (connectionManager_ != null) {
635                 connectionManager_.shutdown();
636                 connectionManager_ = null;
637             }
638         }
639 
640         final int timeout = getTimeout(webRequest);
641         if (timeout != usedOptions_.getTimeout()) {
642             configureTimeout(httpClientBuilder, timeout);
643         }
644 
645         final long connectionTimeToLive = webClient_.getOptions().getConnectionTimeToLive();
646         if (connectionTimeToLive != usedOptions_.getConnectionTimeToLive()) {
647             httpClientBuilder.setConnectionTimeToLive(connectionTimeToLive, TimeUnit.MILLISECONDS);
648             usedOptions_.setConnectionTimeToLive(connectionTimeToLive);
649         }
650 
651         if (connectionManager_ == null) {
652             connectionManager_ = createConnectionManager(httpClientBuilder);
653         }
654         httpClientBuilder.setConnectionManager(connectionManager_);
655 
656         return httpClientBuilder;
657     }
658 
659     private void configureHttpsScheme(final HttpClientBuilder builder) {
660         final WebClientOptions options = webClient_.getOptions();
661 
662         final SSLConnectionSocketFactory socketFactory =
663                 HtmlUnitSSLConnectionSocketFactory.buildSSLSocketFactory(options);
664 
665         builder.setSSLSocketFactory(socketFactory);
666 
667         usedOptions_.setUseInsecureSSL(options.isUseInsecureSSL());
668         usedOptions_.setSSLClientCertificateKeyStore(options.getSSLClientCertificateStore(),
669                         options.getSSLClientCertificatePassword());
670         usedOptions_.setSSLTrustStore(options.getSSLTrustStore());
671         usedOptions_.setSSLClientCipherSuites(options.getSSLClientCipherSuites());
672         usedOptions_.setSSLClientProtocols(options.getSSLClientProtocols());
673         usedOptions_.setProxyConfig(options.getProxyConfig());
674     }
675 
676     private void configureHttpProcessorBuilder(final HttpClientBuilder builder, final WebRequest webRequest) {
677         final HttpProcessorBuilder b = HttpProcessorBuilder.create();
678         for (final HttpRequestInterceptor i : getHttpRequestInterceptors(webRequest)) {
679             b.add(i);
680         }
681 
682         // These are the headers used in HttpClientBuilder, excluding the already added ones
683         // (RequestClientConnControl and RequestAddCookies)
684         b.addAll(new RequestDefaultHeaders(null),
685                 new RequestContent(),
686                 new RequestTargetHost(),
687                 new RequestExpectContinue());
688         b.add(new RequestAcceptEncoding());
689         b.add(new RequestAuthCache());
690 
691         if (!webRequest.hasHint(HttpHint.BlockCookies)) {
692             b.add(new ResponseProcessCookies());
693         }
694         builder.setHttpProcessor(b.build());
695     }
696 
697     /**
698      * Sets the virtual host.
699      * @param virtualHost the virtualHost to set
700      */
701     public void setVirtualHost(final String virtualHost) {
702         virtualHost_ = virtualHost;
703     }
704 
705     /**
706      * Gets the virtual host.
707      * @return virtualHost The current virtualHost
708      */
709     public String getVirtualHost() {
710         return virtualHost_;
711     }
712 
713     /**
714      * Converts an HttpMethod into a {@link WebResponse}.
715      * @param httpResponse the web server's response
716      * @param webRequest the {@link WebRequest}
717      * @param responseBody the {@link DownloadedContent}
718      * @param loadTime the download time
719      * @return a wrapper for the downloaded body.
720      */
721     protected WebResponse makeWebResponse(final HttpResponse httpResponse,
722             final WebRequest webRequest, final DownloadedContent responseBody, final long loadTime) {
723 
724         String statusMessage = httpResponse.getStatusLine().getReasonPhrase();
725         if (statusMessage == null) {
726             statusMessage = "Unknown status message";
727         }
728         final int statusCode = httpResponse.getStatusLine().getStatusCode();
729         final List<NameValuePair> headers = new ArrayList<>();
730         for (final Header header : httpResponse.getAllHeaders()) {
731             headers.add(new NameValuePair(header.getName(), header.getValue()));
732         }
733         final WebResponseData responseData = new WebResponseData(responseBody, statusCode, statusMessage, headers);
734         return newWebResponseInstance(responseData, loadTime, webRequest);
735     }
736 
737     /**
738      * Downloads the response.
739      * This calls {@link #downloadResponseBody(HttpResponse)} and constructs the {@link WebResponse}.
740      * @param httpMethod the HttpUriRequest
741      * @param webRequest the {@link WebRequest}
742      * @param httpResponse the web server's response
743      * @param startTime the download start time
744      * @return a wrapper for the downloaded body.
745      * @throws IOException in case of problem reading/saving the body
746      */
747     protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
748             final WebRequest webRequest, final HttpResponse httpResponse,
749             final long startTime) throws IOException {
750 
751         final DownloadedContent downloadedBody = downloadResponseBody(httpResponse);
752         final long endTime = System.currentTimeMillis();
753 
754         return makeWebResponse(httpResponse, webRequest, downloadedBody, endTime - startTime);
755     }
756 
757     /**
758      * Downloads the response body.
759      * @param httpResponse the web server's response
760      * @return a wrapper for the downloaded body.
761      * @throws IOException in case of problem reading/saving the body
762      */
763     protected DownloadedContent downloadResponseBody(final HttpResponse httpResponse) throws IOException {
764         final HttpEntity httpEntity = httpResponse.getEntity();
765         if (httpEntity == null) {
766             return new DownloadedContent.InMemory(null);
767         }
768 
769         try (InputStream is = httpEntity.getContent()) {
770             return downloadContent(is, webClient_.getOptions().getMaxInMemory(),
771                         webClient_.getOptions().getTempFileDirectory());
772         }
773     }
774 
775     /**
776      * Reads the content of the stream and saves it in memory or on the file system.
777      * @param is the stream to read
778      * @param maxInMemory the maximumBytes to store in memory, after which save to a local file
779      * @param tempFileDirectory the directory to be used or null for the system default
780      * @return a wrapper around the downloaded content
781      * @throws IOException in case of read issues
782      */
783     public static DownloadedContent downloadContent(final InputStream is, final int maxInMemory,
784             final File tempFileDirectory) throws IOException {
785         if (is == null) {
786             return new DownloadedContent.InMemory(null);
787         }
788 
789         try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
790             final byte[] buffer = new byte[1024];
791             int nbRead;
792             try {
793                 while ((nbRead = is.read(buffer)) != -1) {
794                     bos.write(buffer, 0, nbRead);
795                     if (maxInMemory > 0 && bos.size() > maxInMemory) {
796                         // we have exceeded the max for memory, let's write everything to a temporary file
797                         final File file = File.createTempFile("htmlunit", ".tmp", tempFileDirectory);
798                         file.deleteOnExit();
799                         try (OutputStream fos = Files.newOutputStream(file.toPath())) {
800                             bos.writeTo(fos); // what we have already read
801                             IOUtils.copyLarge(is, fos); // what remains from the server response
802                         }
803                         return new DownloadedContent.OnFile(file, true);
804                     }
805                 }
806             }
807             catch (final ConnectionClosedException e) {
808                 LOG.warn("Connection was closed while reading from stream.", e);
809                 return new DownloadedContent.InMemory(bos.toByteArray());
810             }
811             catch (final EOFException e) {
812                 // this might happen with broken gzip content
813                 LOG.warn("EOFException while reading from stream.", e);
814                 return new DownloadedContent.InMemory(bos.toByteArray());
815             }
816 
817             return new DownloadedContent.InMemory(bos.toByteArray());
818         }
819     }
820 
821     /**
822      * Constructs an appropriate WebResponse.
823      * May be overridden by subclasses to return a specialized WebResponse.
824      * @param responseData Data that was send back
825      * @param webRequest the request used to get this response
826      * @param loadTime How long the response took to be sent
827      * @return the new WebResponse
828      */
829     protected WebResponse newWebResponseInstance(
830             final WebResponseData responseData,
831             final long loadTime,
832             final WebRequest webRequest) {
833         return new WebResponse(responseData, webRequest, loadTime);
834     }
835 
836     private List<HttpRequestInterceptor> getHttpRequestInterceptors(final WebRequest webRequest) {
837         final List<HttpRequestInterceptor> list = new ArrayList<>();
838         final Map<String, String> requestHeaders = webRequest.getAdditionalHeaders();
839         final URL url = webRequest.getUrl();
840         final StringBuilder host = new StringBuilder(url.getHost());
841 
842         final int port = url.getPort();
843         if (port > 0 && port != url.getDefaultPort()) {
844             host.append(':').append(port);
845         }
846 
847         // make sure the headers are added in the right order
848         final String[] headerNames = webClient_.getBrowserVersion().getHeaderNamesOrdered();
849         for (final String header : headerNames) {
850             if (HttpHeader.HOST.equals(header)) {
851                 list.add(new HostHeaderHttpRequestInterceptor(host.toString()));
852             }
853             else if (HttpHeader.USER_AGENT.equals(header)) {
854                 String headerValue = webRequest.getAdditionalHeader(HttpHeader.USER_AGENT);
855                 if (headerValue == null) {
856                     headerValue = webClient_.getBrowserVersion().getUserAgent();
857                 }
858                 list.add(new UserAgentHeaderHttpRequestInterceptor(headerValue));
859             }
860             else if (HttpHeader.ACCEPT.equals(header)) {
861                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT);
862                 if (headerValue != null) {
863                     list.add(new AcceptHeaderHttpRequestInterceptor(headerValue));
864                 }
865             }
866             else if (HttpHeader.ACCEPT_LANGUAGE.equals(header)) {
867                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE);
868                 if (headerValue != null) {
869                     list.add(new AcceptLanguageHeaderHttpRequestInterceptor(headerValue));
870                 }
871             }
872             else if (HttpHeader.ACCEPT_ENCODING.equals(header)) {
873                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_ENCODING);
874                 if (headerValue != null) {
875                     list.add(new AcceptEncodingHeaderHttpRequestInterceptor(headerValue));
876                 }
877             }
878             else if (HttpHeader.SEC_FETCH_DEST.equals(header)) {
879                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_DEST);
880                 if (headerValue != null) {
881                     list.add(new SecFetchDestHeaderHttpRequestInterceptor(headerValue));
882                 }
883             }
884             else if (HttpHeader.SEC_FETCH_MODE.equals(header)) {
885                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_MODE);
886                 if (headerValue != null) {
887                     list.add(new SecFetchModeHeaderHttpRequestInterceptor(headerValue));
888                 }
889             }
890             else if (HttpHeader.SEC_FETCH_SITE.equals(header)) {
891                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_SITE);
892                 if (headerValue != null) {
893                     list.add(new SecFetchSiteHeaderHttpRequestInterceptor(headerValue));
894                 }
895             }
896             else if (HttpHeader.SEC_FETCH_USER.equals(header)) {
897                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_USER);
898                 if (headerValue != null) {
899                     list.add(new SecFetchUserHeaderHttpRequestInterceptor(headerValue));
900                 }
901             }
902             else if (HttpHeader.SEC_CH_UA.equals(header)) {
903                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA);
904                 if (headerValue != null) {
905                     list.add(new SecClientHintUserAgentHeaderHttpRequestInterceptor(headerValue));
906                 }
907             }
908             else if (HttpHeader.SEC_CH_UA_MOBILE.equals(header)) {
909                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_MOBILE);
910                 if (headerValue != null) {
911                     list.add(new SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(headerValue));
912                 }
913             }
914             else if (HttpHeader.SEC_CH_UA_PLATFORM.equals(header)) {
915                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_PLATFORM);
916                 if (headerValue != null) {
917                     list.add(new SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(headerValue));
918                 }
919             }
920             else if (HttpHeader.PRIORITY.equals(header)) {
921                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.PRIORITY);
922                 if (headerValue != null) {
923                     list.add(new PriorityHeaderHttpRequestInterceptor(headerValue));
924                 }
925             }
926             else if (HttpHeader.UPGRADE_INSECURE_REQUESTS.equals(header)) {
927                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS);
928                 if (headerValue != null) {
929                     list.add(new UpgradeInsecureRequestHeaderHttpRequestInterceptor(headerValue));
930                 }
931             }
932             else if (HttpHeader.REFERER.equals(header)) {
933                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.REFERER);
934                 if (headerValue != null) {
935                     list.add(new RefererHeaderHttpRequestInterceptor(headerValue));
936                 }
937             }
938             else if (HttpHeader.CONNECTION.equals(header)) {
939                 list.add(new RequestClientConnControl());
940             }
941             else if (HttpHeader.COOKIE.equals(header)) {
942                 if (!webRequest.hasHint(HttpHint.BlockCookies)) {
943                     list.add(new RequestAddCookies());
944                 }
945             }
946             else if (HttpHeader.DNT.equals(header) && webClient_.getOptions().isDoNotTrackEnabled()) {
947                 list.add(new DntHeaderHttpRequestInterceptor("1"));
948             }
949         }
950 
951         // not all browser versions have DNT by default as part of getHeaderNamesOrdered()
952         // so we add it again, in case
953         if (webClient_.getOptions().isDoNotTrackEnabled()) {
954             list.add(new DntHeaderHttpRequestInterceptor("1"));
955         }
956 
957         synchronized (requestHeaders) {
958             list.add(new MultiHttpRequestInterceptor(new HashMap<>(requestHeaders)));
959         }
960         return list;
961     }
962 
963     /** We must have a separate class per header, because of org.apache.http.protocol.ChainBuilder. */
964     private static final class HostHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
965         private final String value_;
966 
967         HostHeaderHttpRequestInterceptor(final String value) {
968             value_ = value;
969         }
970 
971         @Override
972         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
973             request.setHeader(HttpHeader.HOST, value_);
974         }
975     }
976 
977     private static final class UserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
978         private final String value_;
979 
980         UserAgentHeaderHttpRequestInterceptor(final String value) {
981             value_ = value;
982         }
983 
984         @Override
985         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
986             request.setHeader(HttpHeader.USER_AGENT, value_);
987         }
988     }
989 
990     private static final class AcceptHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
991         private final String value_;
992 
993         AcceptHeaderHttpRequestInterceptor(final String value) {
994             value_ = value;
995         }
996 
997         @Override
998         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
999             request.setHeader(HttpHeader.ACCEPT, value_);
1000         }
1001     }
1002 
1003     private static final class AcceptLanguageHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1004         private final String value_;
1005 
1006         AcceptLanguageHeaderHttpRequestInterceptor(final String value) {
1007             value_ = value;
1008         }
1009 
1010         @Override
1011         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1012             request.setHeader(HttpHeader.ACCEPT_LANGUAGE, value_);
1013         }
1014     }
1015 
1016     private static final class UpgradeInsecureRequestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1017         private final String value_;
1018 
1019         UpgradeInsecureRequestHeaderHttpRequestInterceptor(final String value) {
1020             value_ = value;
1021         }
1022 
1023         @Override
1024         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1025             request.setHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS, value_);
1026         }
1027     }
1028 
1029     private static final class AcceptEncodingHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1030         private final String value_;
1031 
1032         AcceptEncodingHeaderHttpRequestInterceptor(final String value) {
1033             value_ = value;
1034         }
1035 
1036         @Override
1037         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1038             request.setHeader("Accept-Encoding", value_);
1039         }
1040     }
1041 
1042     private static final class RefererHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1043         private final String value_;
1044 
1045         RefererHeaderHttpRequestInterceptor(final String value) {
1046             value_ = value;
1047         }
1048 
1049         @Override
1050         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1051             request.setHeader(HttpHeader.REFERER, value_);
1052         }
1053     }
1054 
1055     private static final class DntHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1056         private final String value_;
1057 
1058         DntHeaderHttpRequestInterceptor(final String value) {
1059             value_ = value;
1060         }
1061 
1062         @Override
1063         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1064             request.setHeader(HttpHeader.DNT, value_);
1065         }
1066     }
1067 
1068     private static final class SecFetchModeHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1069         private final String value_;
1070 
1071         SecFetchModeHeaderHttpRequestInterceptor(final String value) {
1072             value_ = value;
1073         }
1074 
1075         @Override
1076         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1077             request.setHeader(HttpHeader.SEC_FETCH_MODE, value_);
1078         }
1079     }
1080 
1081     private static final class SecFetchSiteHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1082         private final String value_;
1083 
1084         SecFetchSiteHeaderHttpRequestInterceptor(final String value) {
1085             value_ = value;
1086         }
1087 
1088         @Override
1089         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1090             request.setHeader(HttpHeader.SEC_FETCH_SITE, value_);
1091         }
1092     }
1093 
1094     private static final class SecFetchUserHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1095         private final String value_;
1096 
1097         SecFetchUserHeaderHttpRequestInterceptor(final String value) {
1098             value_ = value;
1099         }
1100 
1101         @Override
1102         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1103             request.setHeader(HttpHeader.SEC_FETCH_USER, value_);
1104         }
1105     }
1106 
1107     private static final class SecFetchDestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1108         private final String value_;
1109 
1110         SecFetchDestHeaderHttpRequestInterceptor(final String value) {
1111             value_ = value;
1112         }
1113 
1114         @Override
1115         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1116             request.setHeader(HttpHeader.SEC_FETCH_DEST, value_);
1117         }
1118     }
1119 
1120     private static final class SecClientHintUserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1121         private final String value_;
1122 
1123         SecClientHintUserAgentHeaderHttpRequestInterceptor(final String value) {
1124             value_ = value;
1125         }
1126 
1127         @Override
1128         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1129             request.setHeader(HttpHeader.SEC_CH_UA, value_);
1130         }
1131     }
1132 
1133     private static final class SecClientHintUserAgentMobileHeaderHttpRequestInterceptor
1134             implements HttpRequestInterceptor {
1135         private final String value_;
1136 
1137         SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(final String value) {
1138             value_ = value;
1139         }
1140 
1141         @Override
1142         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1143             request.setHeader(HttpHeader.SEC_CH_UA_MOBILE, value_);
1144         }
1145     }
1146 
1147     private static final class SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor
1148             implements HttpRequestInterceptor {
1149         private final String value_;
1150 
1151         SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(final String value) {
1152             value_ = value;
1153         }
1154 
1155         @Override
1156         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1157             request.setHeader(HttpHeader.SEC_CH_UA_PLATFORM, value_);
1158         }
1159     }
1160 
1161     private static final class PriorityHeaderHttpRequestInterceptor
1162             implements HttpRequestInterceptor {
1163         private final String value_;
1164 
1165         PriorityHeaderHttpRequestInterceptor(final String value) {
1166             value_ = value;
1167         }
1168 
1169         @Override
1170         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1171             request.setHeader(HttpHeader.PRIORITY, value_);
1172         }
1173     }
1174 
1175     private static class MultiHttpRequestInterceptor implements HttpRequestInterceptor {
1176         private final Map<String, String> map_;
1177 
1178         MultiHttpRequestInterceptor(final Map<String, String> map) {
1179             map_ = map;
1180         }
1181 
1182         @Override
1183         public void process(final HttpRequest request, final HttpContext context)
1184             throws HttpException, IOException {
1185             for (final Map.Entry<String, String> entry : map_.entrySet()) {
1186                 request.setHeader(entry.getKey(), entry.getValue());
1187             }
1188         }
1189     }
1190 
1191     private static class RequestClientConnControl implements HttpRequestInterceptor {
1192 
1193         private static final String PROXY_CONN_DIRECTIVE = "Proxy-Connection";
1194         private static final String CONN_DIRECTIVE = "Connection";
1195         private static final String CONN_KEEP_ALIVE = "keep-alive";
1196 
1197         /**
1198          * Ctor.
1199          */
1200         RequestClientConnControl() {
1201             super();
1202         }
1203 
1204         @Override
1205         public void process(final HttpRequest request, final HttpContext context)
1206             throws HttpException, IOException {
1207             final String method = request.getRequestLine().getMethod();
1208             if ("CONNECT".equalsIgnoreCase(method)) {
1209                 request.setHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1210                 return;
1211             }
1212 
1213             final HttpClientContext clientContext = HttpClientContext.adapt(context);
1214 
1215             // Obtain the client connection (required)
1216             final RouteInfo route = clientContext.getHttpRoute();
1217             if (route == null) {
1218                 return;
1219             }
1220 
1221             if ((route.getHopCount() == 1 || route.isTunnelled())
1222                     && !request.containsHeader(CONN_DIRECTIVE)) {
1223                 request.addHeader(CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1224             }
1225             if (route.getHopCount() == 2
1226                     && !route.isTunnelled()
1227                     && !request.containsHeader(PROXY_CONN_DIRECTIVE)) {
1228                 request.addHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1229             }
1230         }
1231     }
1232 
1233     /**
1234      * An authentication cache that is synchronized.
1235      */
1236     private static final class SynchronizedAuthCache extends BasicAuthCache {
1237 
1238         /**
1239          * Ctor.
1240          */
1241         SynchronizedAuthCache() {
1242             super();
1243         }
1244 
1245         /**
1246          * {@inheritDoc}
1247          */
1248         @Override
1249         public synchronized void put(final HttpHost host, final AuthScheme authScheme) {
1250             super.put(host, authScheme);
1251         }
1252 
1253         /**
1254          * {@inheritDoc}
1255          */
1256         @Override
1257         public synchronized AuthScheme get(final HttpHost host) {
1258             return super.get(host);
1259         }
1260 
1261         /**
1262          * {@inheritDoc}
1263          */
1264         @Override
1265         public synchronized void remove(final HttpHost host) {
1266             super.remove(host);
1267         }
1268 
1269         /**
1270          * {@inheritDoc}
1271          */
1272         @Override
1273         public synchronized void clear() {
1274             super.clear();
1275         }
1276 
1277         /**
1278          * {@inheritDoc}
1279          */
1280         @Override
1281         public synchronized String toString() {
1282             return super.toString();
1283         }
1284     }
1285 
1286     /**
1287      * {@inheritDoc}
1288      */
1289     @Override
1290     public void close() {
1291         httpClientBuilder_.clear();
1292 
1293         if (connectionManager_ != null) {
1294             connectionManager_.shutdown();
1295             connectionManager_ = null;
1296         }
1297     }
1298 
1299     /**
1300      * Has the exact logic in {@link HttpClientBuilder#build()} which sets the {@code connManager} part,
1301      * but with the ability to configure {@code socketFactory}.
1302      */
1303     private static PoolingHttpClientConnectionManager createConnectionManager(final HttpClientBuilder builder) {
1304         try {
1305             PublicSuffixMatcher publicSuffixMatcher = getField(builder, "publicSuffixMatcher");
1306             if (publicSuffixMatcher == null) {
1307                 publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();
1308             }
1309 
1310             LayeredConnectionSocketFactory sslSocketFactory = getField(builder, "sslSocketFactory");
1311             final SocketConfig defaultSocketConfig = getField(builder, "defaultSocketConfig");
1312             final ConnectionConfig defaultConnectionConfig = getField(builder, "defaultConnectionConfig");
1313             final boolean systemProperties = getField(builder, "systemProperties");
1314             final int maxConnTotal = getField(builder, "maxConnTotal");
1315             final int maxConnPerRoute = getField(builder, "maxConnPerRoute");
1316             HostnameVerifier hostnameVerifier = getField(builder, "hostnameVerifier");
1317             final SSLContext sslcontext = getField(builder, "sslContext");
1318             final DnsResolver dnsResolver = getField(builder, "dnsResolver");
1319             final long connTimeToLive = getField(builder, "connTimeToLive");
1320             final TimeUnit connTimeToLiveTimeUnit = getField(builder, "connTimeToLiveTimeUnit");
1321 
1322             if (sslSocketFactory == null) {
1323                 final String[] supportedProtocols = systemProperties
1324                         ? split(System.getProperty("https.protocols")) : null;
1325                 final String[] supportedCipherSuites = systemProperties
1326                         ? split(System.getProperty("https.cipherSuites")) : null;
1327                 if (hostnameVerifier == null) {
1328                     hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
1329                 }
1330                 if (sslcontext == null) {
1331                     if (systemProperties) {
1332                         sslSocketFactory = new SSLConnectionSocketFactory(
1333                                 (SSLSocketFactory) SSLSocketFactory.getDefault(),
1334                                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
1335                     }
1336                     else {
1337                         sslSocketFactory = new SSLConnectionSocketFactory(
1338                                 SSLContexts.createDefault(),
1339                                 hostnameVerifier);
1340                     }
1341                 }
1342                 else {
1343                     sslSocketFactory = new SSLConnectionSocketFactory(
1344                             sslcontext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
1345                 }
1346             }
1347 
1348             final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
1349                     RegistryBuilder.<ConnectionSocketFactory>create()
1350                         .register("http", new SocksConnectionSocketFactory())
1351                         .register("https", sslSocketFactory)
1352                         .build(),
1353                         null,
1354                         null,
1355                         dnsResolver,
1356                         connTimeToLive,
1357                         connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);
1358             if (defaultSocketConfig != null) {
1359                 poolingmgr.setDefaultSocketConfig(defaultSocketConfig);
1360             }
1361             if (defaultConnectionConfig != null) {
1362                 poolingmgr.setDefaultConnectionConfig(defaultConnectionConfig);
1363             }
1364             if (systemProperties) {
1365                 String s = System.getProperty("http.keepAlive", "true");
1366                 if ("true".equalsIgnoreCase(s)) {
1367                     s = System.getProperty("http.maxConnections", "5");
1368                     final int max = Integer.parseInt(s);
1369                     poolingmgr.setDefaultMaxPerRoute(max);
1370                     poolingmgr.setMaxTotal(2 * max);
1371                 }
1372             }
1373             if (maxConnTotal > 0) {
1374                 poolingmgr.setMaxTotal(maxConnTotal);
1375             }
1376             if (maxConnPerRoute > 0) {
1377                 poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute);
1378             }
1379             return poolingmgr;
1380         }
1381         catch (final IllegalAccessException e) {
1382             throw new RuntimeException(e);
1383         }
1384     }
1385 
1386     private static String[] split(final String s) {
1387         if (TextUtils.isBlank(s)) {
1388             return null;
1389         }
1390         return s.split(" *, *");
1391     }
1392 
1393     @SuppressWarnings("unchecked")
1394     private static <T> T getField(final Object target, final String fieldName) throws IllegalAccessException {
1395         return (T) FieldUtils.readDeclaredField(target, fieldName, true);
1396     }
1397 }