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                     for (int i = 0; i < fileName.length(); i++) {
430                         if (fileName.codePointAt(i) > 127) {
431                             return charset;
432                         }
433                     }
434                 }
435             }
436         }
437         return null;
438     }
439 
440     void buildFilePart(final KeyDataPair pairWithFile, final MultipartEntityBuilder builder) {
441         String mimeType = pairWithFile.getMimeType();
442         if (mimeType == null) {
443             mimeType = MimeType.APPLICATION_OCTET_STREAM;
444         }
445 
446         final ContentType contentType = ContentType.create(mimeType);
447 
448         final File file = pairWithFile.getFile();
449         if (file != null) {
450             String filename = pairWithFile.getFileName();
451             if (filename == null) {
452                 filename = pairWithFile.getFile().getName();
453             }
454             builder.addBinaryBody(pairWithFile.getName(), file, contentType, filename);
455             return;
456         }
457 
458         final byte[] data = pairWithFile.getData();
459         if (data != null) {
460             String filename = pairWithFile.getFileName();
461             if (filename == null) {
462                 filename = pairWithFile.getValue();
463             }
464 
465             builder.addBinaryBody(pairWithFile.getName(), new ByteArrayInputStream(data),
466                     contentType, filename);
467             return;
468         }
469 
470         builder.addPart(pairWithFile.getName(),
471                 // Overridden in order not to have a chunked response.
472                 new InputStreamBody(new ByteArrayInputStream(new byte[0]), contentType, pairWithFile.getValue()) {
473                 @Override
474                 public long getContentLength() {
475                     return 0;
476                 }
477             });
478     }
479 
480     /**
481      * Creates and returns a new HttpClient HTTP method based on the specified parameters.
482      * @param submitMethod the submit method being used
483      * @param uri the uri being used
484      * @return a new HttpClient HTTP method based on the specified parameters
485      */
486     private static HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri) {
487         final HttpRequestBase method;
488         switch (submitMethod) {
489             case GET:
490                 method = new HttpGet(uri);
491                 break;
492 
493             case POST:
494                 method = new HttpPost(uri);
495                 break;
496 
497             case PUT:
498                 method = new HttpPut(uri);
499                 break;
500 
501             case DELETE:
502                 method = new org.htmlunit.httpclient.HttpDelete(uri);
503                 break;
504 
505             case OPTIONS:
506                 method = new org.htmlunit.httpclient.HttpOptions(uri);
507                 break;
508 
509             case HEAD:
510                 method = new HttpHead(uri);
511                 break;
512 
513             case TRACE:
514                 method = new HttpTrace(uri);
515                 break;
516 
517             case PATCH:
518                 method = new HttpPatch(uri);
519                 break;
520 
521             default:
522                 throw new IllegalStateException("Submit method not yet supported: " + submitMethod);
523         }
524         return method;
525     }
526 
527     /**
528      * Lazily initializes the internal HTTP client.
529      *
530      * @return the initialized HTTP client
531      */
532     protected HttpClientBuilder getHttpClientBuilder() {
533         final Thread currentThread = Thread.currentThread();
534         HttpClientBuilder builder = httpClientBuilder_.get(currentThread);
535         if (builder == null) {
536             builder = createHttpClientBuilder();
537 
538             // this factory is required later
539             // to be sure this is done, we do it outside the createHttpClient() call
540             final RegistryBuilder<CookieSpecProvider> registeryBuilder
541                 = RegistryBuilder.<CookieSpecProvider>create()
542                             .register(HACKED_COOKIE_POLICY, htmlUnitCookieSpecProvider_);
543             builder.setDefaultCookieSpecRegistry(registeryBuilder.build());
544 
545             builder.setDefaultCookieStore(new HtmlUnitCookieStore(webClient_.getCookieManager()));
546             builder.setUserAgent(webClient_.getBrowserVersion().getUserAgent());
547             httpClientBuilder_.put(currentThread, builder);
548         }
549 
550         return builder;
551     }
552 
553     /**
554      * Returns the timeout to use for socket and connection timeouts for HttpConnectionManager.
555      * Is overridden to 0 by StreamingWebConnection which keeps reading after a timeout and
556      * must have long running connections explicitly terminated.
557      * @param webRequest the request might have his own timeout
558      * @return the WebClient's timeout
559      */
560     protected int getTimeout(final WebRequest webRequest) {
561         if (webRequest == null || webRequest.getTimeout() < 0) {
562             return webClient_.getOptions().getTimeout();
563         }
564 
565         return webRequest.getTimeout();
566     }
567 
568     /**
569      * Creates the <code>HttpClientBuilder</code> that will be used by this WebClient.
570      * Extensions may override this method in order to create a customized
571      * <code>HttpClientBuilder</code> instance (e.g. with a custom
572      * {@link org.apache.http.conn.ClientConnectionManager} to perform
573      * some tracking; see feature request 1438216).
574      * @return the <code>HttpClientBuilder</code> that will be used by this WebConnection
575      */
576     protected HttpClientBuilder createHttpClientBuilder() {
577         final HttpClientBuilder builder = HttpClientBuilder.create();
578         builder.setRedirectStrategy(new HtmlUnitRedirectStrategie());
579         configureTimeout(builder, getTimeout(null));
580         configureHttpsScheme(builder);
581         builder.setMaxConnPerRoute(6);
582 
583         builder.setConnectionManagerShared(true);
584         return builder;
585     }
586 
587     private void configureTimeout(final HttpClientBuilder builder, final int timeout) {
588         final InetAddress localAddress = webClient_.getOptions().getLocalAddress();
589         final RequestConfig.Builder requestBuilder = createRequestConfigBuilder(timeout, localAddress);
590         builder.setDefaultRequestConfig(requestBuilder.build());
591 
592         builder.setDefaultSocketConfig(createSocketConfigBuilder(timeout).build());
593 
594         getHttpContext().removeAttribute(HttpClientContext.REQUEST_CONFIG);
595         usedOptions_.setTimeout(timeout);
596     }
597 
598     private static RequestConfig.Builder createRequestConfigBuilder(final int timeout, final InetAddress localAddress) {
599         return RequestConfig.custom()
600                 .setCookieSpec(HACKED_COOKIE_POLICY)
601                 .setRedirectsEnabled(false)
602                 .setLocalAddress(localAddress)
603 
604                 // timeout
605                 .setConnectTimeout(timeout)
606                 .setConnectionRequestTimeout(timeout)
607                 .setSocketTimeout(timeout);
608     }
609 
610     private static SocketConfig.Builder createSocketConfigBuilder(final int timeout) {
611         return SocketConfig.custom()
612                 // timeout
613                 .setSoTimeout(timeout);
614     }
615 
616     /**
617      * React on changes that may have occurred on the WebClient settings.
618      * Registering as a listener would be probably better.
619      */
620     private HttpClientBuilder reconfigureHttpClientIfNeeded(final HttpClientBuilder httpClientBuilder,
621             final WebRequest webRequest) {
622         final WebClientOptions options = webClient_.getOptions();
623 
624         // register new SSL factory only if settings have changed
625         if (options.isUseInsecureSSL() != usedOptions_.isUseInsecureSSL()
626                 || options.getSSLClientCertificateStore() != usedOptions_.getSSLClientCertificateStore()
627                 || options.getSSLTrustStore() != usedOptions_.getSSLTrustStore()
628                 || options.getSSLClientCipherSuites() != usedOptions_.getSSLClientCipherSuites()
629                 || options.getSSLClientProtocols() != usedOptions_.getSSLClientProtocols()
630                 || options.getProxyConfig() != usedOptions_.getProxyConfig()) {
631             configureHttpsScheme(httpClientBuilder);
632 
633             if (connectionManager_ != null) {
634                 connectionManager_.shutdown();
635                 connectionManager_ = null;
636             }
637         }
638 
639         final int timeout = getTimeout(webRequest);
640         if (timeout != usedOptions_.getTimeout()) {
641             configureTimeout(httpClientBuilder, timeout);
642         }
643 
644         final long connectionTimeToLive = webClient_.getOptions().getConnectionTimeToLive();
645         if (connectionTimeToLive != usedOptions_.getConnectionTimeToLive()) {
646             httpClientBuilder.setConnectionTimeToLive(connectionTimeToLive, TimeUnit.MILLISECONDS);
647             usedOptions_.setConnectionTimeToLive(connectionTimeToLive);
648         }
649 
650         if (connectionManager_ == null) {
651             connectionManager_ = createConnectionManager(httpClientBuilder);
652         }
653         httpClientBuilder.setConnectionManager(connectionManager_);
654 
655         return httpClientBuilder;
656     }
657 
658     private void configureHttpsScheme(final HttpClientBuilder builder) {
659         final WebClientOptions options = webClient_.getOptions();
660 
661         final SSLConnectionSocketFactory socketFactory =
662                 HtmlUnitSSLConnectionSocketFactory.buildSSLSocketFactory(options);
663 
664         builder.setSSLSocketFactory(socketFactory);
665 
666         usedOptions_.setUseInsecureSSL(options.isUseInsecureSSL());
667         usedOptions_.setSSLClientCertificateKeyStore(options.getSSLClientCertificateStore(),
668                         options.getSSLClientCertificatePassword());
669         usedOptions_.setSSLTrustStore(options.getSSLTrustStore());
670         usedOptions_.setSSLClientCipherSuites(options.getSSLClientCipherSuites());
671         usedOptions_.setSSLClientProtocols(options.getSSLClientProtocols());
672         usedOptions_.setProxyConfig(options.getProxyConfig());
673     }
674 
675     private void configureHttpProcessorBuilder(final HttpClientBuilder builder, final WebRequest webRequest) {
676         final HttpProcessorBuilder b = HttpProcessorBuilder.create();
677         for (final HttpRequestInterceptor i : getHttpRequestInterceptors(webRequest)) {
678             b.add(i);
679         }
680 
681         // These are the headers used in HttpClientBuilder, excluding the already added ones
682         // (RequestClientConnControl and RequestAddCookies)
683         b.addAll(new RequestDefaultHeaders(null),
684                 new RequestContent(),
685                 new RequestTargetHost(),
686                 new RequestExpectContinue());
687         b.add(new RequestAcceptEncoding());
688         b.add(new RequestAuthCache());
689 
690         if (!webRequest.hasHint(HttpHint.BlockCookies)) {
691             b.add(new ResponseProcessCookies());
692         }
693         builder.setHttpProcessor(b.build());
694     }
695 
696     /**
697      * Sets the virtual host.
698      * @param virtualHost the virtualHost to set
699      */
700     public void setVirtualHost(final String virtualHost) {
701         virtualHost_ = virtualHost;
702     }
703 
704     /**
705      * Gets the virtual host.
706      * @return virtualHost The current virtualHost
707      */
708     public String getVirtualHost() {
709         return virtualHost_;
710     }
711 
712     /**
713      * Converts an HttpMethod into a {@link WebResponse}.
714      * @param httpResponse the web server's response
715      * @param webRequest the {@link WebRequest}
716      * @param responseBody the {@link DownloadedContent}
717      * @param loadTime the download time
718      * @return a wrapper for the downloaded body.
719      */
720     protected WebResponse makeWebResponse(final HttpResponse httpResponse,
721             final WebRequest webRequest, final DownloadedContent responseBody, final long loadTime) {
722 
723         String statusMessage = httpResponse.getStatusLine().getReasonPhrase();
724         if (statusMessage == null) {
725             statusMessage = "Unknown status message";
726         }
727         final int statusCode = httpResponse.getStatusLine().getStatusCode();
728         final List<NameValuePair> headers = new ArrayList<>();
729         for (final Header header : httpResponse.getAllHeaders()) {
730             headers.add(new NameValuePair(header.getName(), header.getValue()));
731         }
732         final WebResponseData responseData = new WebResponseData(responseBody, statusCode, statusMessage, headers);
733         return newWebResponseInstance(responseData, loadTime, webRequest);
734     }
735 
736     /**
737      * Downloads the response.
738      * This calls {@link #downloadResponseBody(HttpResponse)} and constructs the {@link WebResponse}.
739      * @param httpMethod the HttpUriRequest
740      * @param webRequest the {@link WebRequest}
741      * @param httpResponse the web server's response
742      * @param startTime the download start time
743      * @return a wrapper for the downloaded body.
744      * @throws IOException in case of problem reading/saving the body
745      */
746     protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
747             final WebRequest webRequest, final HttpResponse httpResponse,
748             final long startTime) throws IOException {
749 
750         final DownloadedContent downloadedBody = downloadResponseBody(httpResponse);
751         final long endTime = System.currentTimeMillis();
752 
753         return makeWebResponse(httpResponse, webRequest, downloadedBody, endTime - startTime);
754     }
755 
756     /**
757      * Downloads the response body.
758      * @param httpResponse the web server's response
759      * @return a wrapper for the downloaded body.
760      * @throws IOException in case of problem reading/saving the body
761      */
762     protected DownloadedContent downloadResponseBody(final HttpResponse httpResponse) throws IOException {
763         final HttpEntity httpEntity = httpResponse.getEntity();
764         if (httpEntity == null) {
765             return new DownloadedContent.InMemory(null);
766         }
767 
768         try (InputStream is = httpEntity.getContent()) {
769             return downloadContent(is, webClient_.getOptions().getMaxInMemory(),
770                         webClient_.getOptions().getTempFileDirectory());
771         }
772     }
773 
774     /**
775      * Reads the content of the stream and saves it in memory or on the file system.
776      * @param is the stream to read
777      * @param maxInMemory the maximumBytes to store in memory, after which save to a local file
778      * @param tempFileDirectory the directory to be used or null for the system default
779      * @return a wrapper around the downloaded content
780      * @throws IOException in case of read issues
781      */
782     public static DownloadedContent downloadContent(final InputStream is, final int maxInMemory,
783             final File tempFileDirectory) throws IOException {
784         if (is == null) {
785             return new DownloadedContent.InMemory(null);
786         }
787 
788         try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
789             final byte[] buffer = new byte[1024];
790             int nbRead;
791             try {
792                 while ((nbRead = is.read(buffer)) != -1) {
793                     bos.write(buffer, 0, nbRead);
794                     if (maxInMemory > 0 && bos.size() > maxInMemory) {
795                         // we have exceeded the max for memory, let's write everything to a temporary file
796                         final File file = File.createTempFile("htmlunit", ".tmp", tempFileDirectory);
797                         file.deleteOnExit();
798                         try (OutputStream fos = Files.newOutputStream(file.toPath())) {
799                             bos.writeTo(fos); // what we have already read
800                             IOUtils.copyLarge(is, fos); // what remains from the server response
801                         }
802                         return new DownloadedContent.OnFile(file, true);
803                     }
804                 }
805             }
806             catch (final ConnectionClosedException e) {
807                 LOG.warn("Connection was closed while reading from stream.", e);
808                 return new DownloadedContent.InMemory(bos.toByteArray());
809             }
810             catch (final EOFException e) {
811                 // this might happen with broken gzip content
812                 LOG.warn("EOFException while reading from stream.", e);
813                 return new DownloadedContent.InMemory(bos.toByteArray());
814             }
815 
816             return new DownloadedContent.InMemory(bos.toByteArray());
817         }
818     }
819 
820     /**
821      * Constructs an appropriate WebResponse.
822      * May be overridden by subclasses to return a specialized WebResponse.
823      * @param responseData Data that was send back
824      * @param webRequest the request used to get this response
825      * @param loadTime How long the response took to be sent
826      * @return the new WebResponse
827      */
828     protected WebResponse newWebResponseInstance(
829             final WebResponseData responseData,
830             final long loadTime,
831             final WebRequest webRequest) {
832         return new WebResponse(responseData, webRequest, loadTime);
833     }
834 
835     private List<HttpRequestInterceptor> getHttpRequestInterceptors(final WebRequest webRequest) {
836         final List<HttpRequestInterceptor> list = new ArrayList<>();
837         final Map<String, String> requestHeaders = webRequest.getAdditionalHeaders();
838         final URL url = webRequest.getUrl();
839         final StringBuilder host = new StringBuilder(url.getHost());
840 
841         final int port = url.getPort();
842         if (port > 0 && port != url.getDefaultPort()) {
843             host.append(':').append(port);
844         }
845 
846         // make sure the headers are added in the right order
847         final String[] headerNames = webClient_.getBrowserVersion().getHeaderNamesOrdered();
848         for (final String header : headerNames) {
849             if (HttpHeader.HOST.equals(header)) {
850                 list.add(new HostHeaderHttpRequestInterceptor(host.toString()));
851             }
852             else if (HttpHeader.USER_AGENT.equals(header)) {
853                 String headerValue = webRequest.getAdditionalHeader(HttpHeader.USER_AGENT);
854                 if (headerValue == null) {
855                     headerValue = webClient_.getBrowserVersion().getUserAgent();
856                 }
857                 list.add(new UserAgentHeaderHttpRequestInterceptor(headerValue));
858             }
859             else if (HttpHeader.ACCEPT.equals(header)) {
860                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT);
861                 if (headerValue != null) {
862                     list.add(new AcceptHeaderHttpRequestInterceptor(headerValue));
863                 }
864             }
865             else if (HttpHeader.ACCEPT_LANGUAGE.equals(header)) {
866                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE);
867                 if (headerValue != null) {
868                     list.add(new AcceptLanguageHeaderHttpRequestInterceptor(headerValue));
869                 }
870             }
871             else if (HttpHeader.ACCEPT_ENCODING.equals(header)) {
872                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_ENCODING);
873                 if (headerValue != null) {
874                     list.add(new AcceptEncodingHeaderHttpRequestInterceptor(headerValue));
875                 }
876             }
877             else if (HttpHeader.SEC_FETCH_DEST.equals(header)) {
878                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_DEST);
879                 if (headerValue != null) {
880                     list.add(new SecFetchDestHeaderHttpRequestInterceptor(headerValue));
881                 }
882             }
883             else if (HttpHeader.SEC_FETCH_MODE.equals(header)) {
884                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_MODE);
885                 if (headerValue != null) {
886                     list.add(new SecFetchModeHeaderHttpRequestInterceptor(headerValue));
887                 }
888             }
889             else if (HttpHeader.SEC_FETCH_SITE.equals(header)) {
890                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_SITE);
891                 if (headerValue != null) {
892                     list.add(new SecFetchSiteHeaderHttpRequestInterceptor(headerValue));
893                 }
894             }
895             else if (HttpHeader.SEC_FETCH_USER.equals(header)) {
896                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_USER);
897                 if (headerValue != null) {
898                     list.add(new SecFetchUserHeaderHttpRequestInterceptor(headerValue));
899                 }
900             }
901             else if (HttpHeader.SEC_CH_UA.equals(header)) {
902                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA);
903                 if (headerValue != null) {
904                     list.add(new SecClientHintUserAgentHeaderHttpRequestInterceptor(headerValue));
905                 }
906             }
907             else if (HttpHeader.SEC_CH_UA_MOBILE.equals(header)) {
908                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_MOBILE);
909                 if (headerValue != null) {
910                     list.add(new SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(headerValue));
911                 }
912             }
913             else if (HttpHeader.SEC_CH_UA_PLATFORM.equals(header)) {
914                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_PLATFORM);
915                 if (headerValue != null) {
916                     list.add(new SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(headerValue));
917                 }
918             }
919             else if (HttpHeader.PRIORITY.equals(header)) {
920                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.PRIORITY);
921                 if (headerValue != null) {
922                     list.add(new PriorityHeaderHttpRequestInterceptor(headerValue));
923                 }
924             }
925             else if (HttpHeader.UPGRADE_INSECURE_REQUESTS.equals(header)) {
926                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS);
927                 if (headerValue != null) {
928                     list.add(new UpgradeInsecureRequestHeaderHttpRequestInterceptor(headerValue));
929                 }
930             }
931             else if (HttpHeader.REFERER.equals(header)) {
932                 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.REFERER);
933                 if (headerValue != null) {
934                     list.add(new RefererHeaderHttpRequestInterceptor(headerValue));
935                 }
936             }
937             else if (HttpHeader.CONNECTION.equals(header)) {
938                 list.add(new RequestClientConnControl());
939             }
940             else if (HttpHeader.COOKIE.equals(header)) {
941                 if (!webRequest.hasHint(HttpHint.BlockCookies)) {
942                     list.add(new RequestAddCookies());
943                 }
944             }
945             else if (HttpHeader.DNT.equals(header) && webClient_.getOptions().isDoNotTrackEnabled()) {
946                 list.add(new DntHeaderHttpRequestInterceptor("1"));
947             }
948         }
949 
950         // not all browser versions have DNT by default as part of getHeaderNamesOrdered()
951         // so we add it again, in case
952         if (webClient_.getOptions().isDoNotTrackEnabled()) {
953             list.add(new DntHeaderHttpRequestInterceptor("1"));
954         }
955 
956         synchronized (requestHeaders) {
957             list.add(new MultiHttpRequestInterceptor(new HashMap<>(requestHeaders)));
958         }
959         return list;
960     }
961 
962     /** We must have a separate class per header, because of org.apache.http.protocol.ChainBuilder. */
963     private static final class HostHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
964         private final String value_;
965 
966         HostHeaderHttpRequestInterceptor(final String value) {
967             value_ = value;
968         }
969 
970         @Override
971         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
972             request.setHeader(HttpHeader.HOST, value_);
973         }
974     }
975 
976     private static final class UserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
977         private final String value_;
978 
979         UserAgentHeaderHttpRequestInterceptor(final String value) {
980             value_ = value;
981         }
982 
983         @Override
984         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
985             request.setHeader(HttpHeader.USER_AGENT, value_);
986         }
987     }
988 
989     private static final class AcceptHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
990         private final String value_;
991 
992         AcceptHeaderHttpRequestInterceptor(final String value) {
993             value_ = value;
994         }
995 
996         @Override
997         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
998             request.setHeader(HttpHeader.ACCEPT, value_);
999         }
1000     }
1001 
1002     private static final class AcceptLanguageHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1003         private final String value_;
1004 
1005         AcceptLanguageHeaderHttpRequestInterceptor(final String value) {
1006             value_ = value;
1007         }
1008 
1009         @Override
1010         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1011             request.setHeader(HttpHeader.ACCEPT_LANGUAGE, value_);
1012         }
1013     }
1014 
1015     private static final class UpgradeInsecureRequestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1016         private final String value_;
1017 
1018         UpgradeInsecureRequestHeaderHttpRequestInterceptor(final String value) {
1019             value_ = value;
1020         }
1021 
1022         @Override
1023         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1024             request.setHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS, value_);
1025         }
1026     }
1027 
1028     private static final class AcceptEncodingHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1029         private final String value_;
1030 
1031         AcceptEncodingHeaderHttpRequestInterceptor(final String value) {
1032             value_ = value;
1033         }
1034 
1035         @Override
1036         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1037             request.setHeader("Accept-Encoding", value_);
1038         }
1039     }
1040 
1041     private static final class RefererHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1042         private final String value_;
1043 
1044         RefererHeaderHttpRequestInterceptor(final String value) {
1045             value_ = value;
1046         }
1047 
1048         @Override
1049         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1050             request.setHeader(HttpHeader.REFERER, value_);
1051         }
1052     }
1053 
1054     private static final class DntHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1055         private final String value_;
1056 
1057         DntHeaderHttpRequestInterceptor(final String value) {
1058             value_ = value;
1059         }
1060 
1061         @Override
1062         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1063             request.setHeader(HttpHeader.DNT, value_);
1064         }
1065     }
1066 
1067     private static final class SecFetchModeHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1068         private final String value_;
1069 
1070         SecFetchModeHeaderHttpRequestInterceptor(final String value) {
1071             value_ = value;
1072         }
1073 
1074         @Override
1075         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1076             request.setHeader(HttpHeader.SEC_FETCH_MODE, value_);
1077         }
1078     }
1079 
1080     private static final class SecFetchSiteHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1081         private final String value_;
1082 
1083         SecFetchSiteHeaderHttpRequestInterceptor(final String value) {
1084             value_ = value;
1085         }
1086 
1087         @Override
1088         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1089             request.setHeader(HttpHeader.SEC_FETCH_SITE, value_);
1090         }
1091     }
1092 
1093     private static final class SecFetchUserHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1094         private final String value_;
1095 
1096         SecFetchUserHeaderHttpRequestInterceptor(final String value) {
1097             value_ = value;
1098         }
1099 
1100         @Override
1101         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1102             request.setHeader(HttpHeader.SEC_FETCH_USER, value_);
1103         }
1104     }
1105 
1106     private static final class SecFetchDestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1107         private final String value_;
1108 
1109         SecFetchDestHeaderHttpRequestInterceptor(final String value) {
1110             value_ = value;
1111         }
1112 
1113         @Override
1114         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1115             request.setHeader(HttpHeader.SEC_FETCH_DEST, value_);
1116         }
1117     }
1118 
1119     private static final class SecClientHintUserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1120         private final String value_;
1121 
1122         SecClientHintUserAgentHeaderHttpRequestInterceptor(final String value) {
1123             value_ = value;
1124         }
1125 
1126         @Override
1127         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1128             request.setHeader(HttpHeader.SEC_CH_UA, value_);
1129         }
1130     }
1131 
1132     private static final class SecClientHintUserAgentMobileHeaderHttpRequestInterceptor
1133             implements HttpRequestInterceptor {
1134         private final String value_;
1135 
1136         SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(final String value) {
1137             value_ = value;
1138         }
1139 
1140         @Override
1141         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1142             request.setHeader(HttpHeader.SEC_CH_UA_MOBILE, value_);
1143         }
1144     }
1145 
1146     private static final class SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor
1147             implements HttpRequestInterceptor {
1148         private final String value_;
1149 
1150         SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(final String value) {
1151             value_ = value;
1152         }
1153 
1154         @Override
1155         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1156             request.setHeader(HttpHeader.SEC_CH_UA_PLATFORM, value_);
1157         }
1158     }
1159 
1160     private static final class PriorityHeaderHttpRequestInterceptor
1161             implements HttpRequestInterceptor {
1162         private final String value_;
1163 
1164         PriorityHeaderHttpRequestInterceptor(final String value) {
1165             value_ = value;
1166         }
1167 
1168         @Override
1169         public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1170             request.setHeader(HttpHeader.PRIORITY, value_);
1171         }
1172     }
1173 
1174     private static class MultiHttpRequestInterceptor implements HttpRequestInterceptor {
1175         private final Map<String, String> map_;
1176 
1177         MultiHttpRequestInterceptor(final Map<String, String> map) {
1178             map_ = map;
1179         }
1180 
1181         @Override
1182         public void process(final HttpRequest request, final HttpContext context)
1183             throws HttpException, IOException {
1184             for (final Map.Entry<String, String> entry : map_.entrySet()) {
1185                 request.setHeader(entry.getKey(), entry.getValue());
1186             }
1187         }
1188     }
1189 
1190     private static class RequestClientConnControl implements HttpRequestInterceptor {
1191 
1192         private static final String PROXY_CONN_DIRECTIVE = "Proxy-Connection";
1193         private static final String CONN_DIRECTIVE = "Connection";
1194         private static final String CONN_KEEP_ALIVE = "keep-alive";
1195 
1196         /**
1197          * Ctor.
1198          */
1199         RequestClientConnControl() {
1200             super();
1201         }
1202 
1203         @Override
1204         public void process(final HttpRequest request, final HttpContext context)
1205             throws HttpException, IOException {
1206             final String method = request.getRequestLine().getMethod();
1207             if ("CONNECT".equalsIgnoreCase(method)) {
1208                 request.setHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1209                 return;
1210             }
1211 
1212             final HttpClientContext clientContext = HttpClientContext.adapt(context);
1213 
1214             // Obtain the client connection (required)
1215             final RouteInfo route = clientContext.getHttpRoute();
1216             if (route == null) {
1217                 return;
1218             }
1219 
1220             if ((route.getHopCount() == 1 || route.isTunnelled())
1221                     && !request.containsHeader(CONN_DIRECTIVE)) {
1222                 request.addHeader(CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1223             }
1224             if (route.getHopCount() == 2
1225                     && !route.isTunnelled()
1226                     && !request.containsHeader(PROXY_CONN_DIRECTIVE)) {
1227                 request.addHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1228             }
1229         }
1230     }
1231 
1232     /**
1233      * An authentication cache that is synchronized.
1234      */
1235     private static final class SynchronizedAuthCache extends BasicAuthCache {
1236 
1237         /**
1238          * Ctor.
1239          */
1240         SynchronizedAuthCache() {
1241             super();
1242         }
1243 
1244         /**
1245          * {@inheritDoc}
1246          */
1247         @Override
1248         public synchronized void put(final HttpHost host, final AuthScheme authScheme) {
1249             super.put(host, authScheme);
1250         }
1251 
1252         /**
1253          * {@inheritDoc}
1254          */
1255         @Override
1256         public synchronized AuthScheme get(final HttpHost host) {
1257             return super.get(host);
1258         }
1259 
1260         /**
1261          * {@inheritDoc}
1262          */
1263         @Override
1264         public synchronized void remove(final HttpHost host) {
1265             super.remove(host);
1266         }
1267 
1268         /**
1269          * {@inheritDoc}
1270          */
1271         @Override
1272         public synchronized void clear() {
1273             super.clear();
1274         }
1275 
1276         /**
1277          * {@inheritDoc}
1278          */
1279         @Override
1280         public synchronized String toString() {
1281             return super.toString();
1282         }
1283     }
1284 
1285     /**
1286      * {@inheritDoc}
1287      */
1288     @Override
1289     public void close() {
1290         httpClientBuilder_.clear();
1291 
1292         if (connectionManager_ != null) {
1293             connectionManager_.shutdown();
1294             connectionManager_ = null;
1295         }
1296     }
1297 
1298     /**
1299      * Has the exact logic in {@link HttpClientBuilder#build()} which sets the {@code connManager} part,
1300      * but with the ability to configure {@code socketFactory}.
1301      */
1302     private static PoolingHttpClientConnectionManager createConnectionManager(final HttpClientBuilder builder) {
1303         try {
1304             PublicSuffixMatcher publicSuffixMatcher = getField(builder, "publicSuffixMatcher");
1305             if (publicSuffixMatcher == null) {
1306                 publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();
1307             }
1308 
1309             LayeredConnectionSocketFactory sslSocketFactory = getField(builder, "sslSocketFactory");
1310             final SocketConfig defaultSocketConfig = getField(builder, "defaultSocketConfig");
1311             final ConnectionConfig defaultConnectionConfig = getField(builder, "defaultConnectionConfig");
1312             final boolean systemProperties = getField(builder, "systemProperties");
1313             final int maxConnTotal = getField(builder, "maxConnTotal");
1314             final int maxConnPerRoute = getField(builder, "maxConnPerRoute");
1315             HostnameVerifier hostnameVerifier = getField(builder, "hostnameVerifier");
1316             final SSLContext sslcontext = getField(builder, "sslContext");
1317             final DnsResolver dnsResolver = getField(builder, "dnsResolver");
1318             final long connTimeToLive = getField(builder, "connTimeToLive");
1319             final TimeUnit connTimeToLiveTimeUnit = getField(builder, "connTimeToLiveTimeUnit");
1320 
1321             if (sslSocketFactory == null) {
1322                 final String[] supportedProtocols = systemProperties
1323                         ? split(System.getProperty("https.protocols")) : null;
1324                 final String[] supportedCipherSuites = systemProperties
1325                         ? split(System.getProperty("https.cipherSuites")) : null;
1326                 if (hostnameVerifier == null) {
1327                     hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
1328                 }
1329                 if (sslcontext == null) {
1330                     if (systemProperties) {
1331                         sslSocketFactory = new SSLConnectionSocketFactory(
1332                                 (SSLSocketFactory) SSLSocketFactory.getDefault(),
1333                                 supportedProtocols, supportedCipherSuites, hostnameVerifier);
1334                     }
1335                     else {
1336                         sslSocketFactory = new SSLConnectionSocketFactory(
1337                                 SSLContexts.createDefault(),
1338                                 hostnameVerifier);
1339                     }
1340                 }
1341                 else {
1342                     sslSocketFactory = new SSLConnectionSocketFactory(
1343                             sslcontext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
1344                 }
1345             }
1346 
1347             final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
1348                     RegistryBuilder.<ConnectionSocketFactory>create()
1349                         .register("http", new SocksConnectionSocketFactory())
1350                         .register("https", sslSocketFactory)
1351                         .build(),
1352                         null,
1353                         null,
1354                         dnsResolver,
1355                         connTimeToLive,
1356                         connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);
1357             if (defaultSocketConfig != null) {
1358                 poolingmgr.setDefaultSocketConfig(defaultSocketConfig);
1359             }
1360             if (defaultConnectionConfig != null) {
1361                 poolingmgr.setDefaultConnectionConfig(defaultConnectionConfig);
1362             }
1363             if (systemProperties) {
1364                 String s = System.getProperty("http.keepAlive", "true");
1365                 if ("true".equalsIgnoreCase(s)) {
1366                     s = System.getProperty("http.maxConnections", "5");
1367                     final int max = Integer.parseInt(s);
1368                     poolingmgr.setDefaultMaxPerRoute(max);
1369                     poolingmgr.setMaxTotal(2 * max);
1370                 }
1371             }
1372             if (maxConnTotal > 0) {
1373                 poolingmgr.setMaxTotal(maxConnTotal);
1374             }
1375             if (maxConnPerRoute > 0) {
1376                 poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute);
1377             }
1378             return poolingmgr;
1379         }
1380         catch (final IllegalAccessException e) {
1381             throw new RuntimeException(e);
1382         }
1383     }
1384 
1385     private static String[] split(final String s) {
1386         if (TextUtils.isBlank(s)) {
1387             return null;
1388         }
1389         return s.split(" *, *");
1390     }
1391 
1392     @SuppressWarnings("unchecked")
1393     private static <T> T getField(final Object target, final String fieldName) throws IllegalAccessException {
1394         return (T) FieldUtils.readDeclaredField(target, fieldName, true);
1395     }
1396 }