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