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