View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.transport.apache;
20  
21  import javax.net.ssl.SSLSession;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InterruptedIOException;
26  import java.io.OutputStream;
27  import java.io.UncheckedIOException;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.nio.charset.Charset;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.StandardCopyOption;
34  import java.util.Date;
35  import java.util.List;
36  import java.util.Map;
37  import java.util.Set;
38  import java.util.concurrent.ConcurrentHashMap;
39  import java.util.function.Function;
40  import java.util.regex.Matcher;
41  
42  import org.apache.http.Header;
43  import org.apache.http.HttpClientConnection;
44  import org.apache.http.HttpEntity;
45  import org.apache.http.HttpEntityEnclosingRequest;
46  import org.apache.http.HttpException;
47  import org.apache.http.HttpHeaders;
48  import org.apache.http.HttpHost;
49  import org.apache.http.HttpRequest;
50  import org.apache.http.HttpResponse;
51  import org.apache.http.HttpStatus;
52  import org.apache.http.ProtocolVersion;
53  import org.apache.http.auth.AuthScheme;
54  import org.apache.http.auth.AuthSchemeProvider;
55  import org.apache.http.auth.AuthScope;
56  import org.apache.http.client.AuthCache;
57  import org.apache.http.client.CredentialsProvider;
58  import org.apache.http.client.HttpRequestRetryHandler;
59  import org.apache.http.client.HttpResponseException;
60  import org.apache.http.client.ServiceUnavailableRetryStrategy;
61  import org.apache.http.client.config.AuthSchemes;
62  import org.apache.http.client.config.CookieSpecs;
63  import org.apache.http.client.config.RequestConfig;
64  import org.apache.http.client.methods.CloseableHttpResponse;
65  import org.apache.http.client.methods.HttpGet;
66  import org.apache.http.client.methods.HttpHead;
67  import org.apache.http.client.methods.HttpOptions;
68  import org.apache.http.client.methods.HttpPut;
69  import org.apache.http.client.methods.HttpUriRequest;
70  import org.apache.http.client.utils.DateUtils;
71  import org.apache.http.client.utils.URIUtils;
72  import org.apache.http.config.Registry;
73  import org.apache.http.config.RegistryBuilder;
74  import org.apache.http.config.SocketConfig;
75  import org.apache.http.conn.ManagedHttpClientConnection;
76  import org.apache.http.entity.AbstractHttpEntity;
77  import org.apache.http.entity.ByteArrayEntity;
78  import org.apache.http.impl.NoConnectionReuseStrategy;
79  import org.apache.http.impl.auth.BasicScheme;
80  import org.apache.http.impl.auth.BasicSchemeFactory;
81  import org.apache.http.impl.auth.DigestSchemeFactory;
82  import org.apache.http.impl.auth.KerberosSchemeFactory;
83  import org.apache.http.impl.auth.NTLMSchemeFactory;
84  import org.apache.http.impl.auth.SPNegoSchemeFactory;
85  import org.apache.http.impl.client.CloseableHttpClient;
86  import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
87  import org.apache.http.impl.client.HttpClientBuilder;
88  import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
89  import org.apache.http.protocol.HttpContext;
90  import org.apache.http.protocol.HttpCoreContext;
91  import org.apache.http.protocol.HttpRequestExecutor;
92  import org.apache.http.util.EntityUtils;
93  import org.eclipse.aether.Keys;
94  import org.eclipse.aether.RepositorySystemSession;
95  import org.eclipse.aether.repository.AuthenticationContext;
96  import org.eclipse.aether.repository.Proxy;
97  import org.eclipse.aether.repository.RemoteRepository;
98  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
99  import org.eclipse.aether.spi.connector.transport.GetTask;
100 import org.eclipse.aether.spi.connector.transport.PeekTask;
101 import org.eclipse.aether.spi.connector.transport.PutTask;
102 import org.eclipse.aether.spi.connector.transport.TransportListener;
103 import org.eclipse.aether.spi.connector.transport.TransportTask;
104 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
105 import org.eclipse.aether.spi.connector.transport.http.HttpTransportPropertiesBuilder;
106 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
107 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
108 import org.eclipse.aether.spi.io.PathProcessor;
109 import org.eclipse.aether.transfer.HttpTransportProperty.HttpVersion;
110 import org.eclipse.aether.transfer.NoTransporterException;
111 import org.eclipse.aether.transfer.TransferCancelledException;
112 import org.eclipse.aether.transfer.TransferEvent;
113 import org.eclipse.aether.util.ConfigUtils;
114 import org.eclipse.aether.util.StringDigestUtil;
115 import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils;
116 import org.slf4j.Logger;
117 import org.slf4j.LoggerFactory;
118 
119 import static java.util.Objects.requireNonNull;
120 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE_PATTERN;
121 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_FOLLOW_INSECURE_REDIRECTS;
122 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_FOLLOW_REDIRECTS;
123 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_HTTP_RETRY_HANDLER_NAME;
124 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED;
125 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_MAX_REDIRECTS;
126 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.CONFIG_PROP_USE_SYSTEM_PROPERTIES;
127 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.DEFAULT_FOLLOW_INSECURE_REDIRECTS;
128 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.DEFAULT_FOLLOW_REDIRECTS;
129 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.DEFAULT_HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED;
130 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.DEFAULT_MAX_REDIRECTS;
131 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.DEFAULT_USE_SYSTEM_PROPERTIES;
132 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.HTTP_RETRY_HANDLER_NAME_DEFAULT;
133 import static org.eclipse.aether.transport.apache.ApacheTransporterConfigurationKeys.HTTP_RETRY_HANDLER_NAME_STANDARD;
134 
135 /**
136  * A transporter for HTTP/HTTPS.
137  */
138 final class ApacheTransporter extends AbstractTransporter implements HttpTransporter {
139     /**
140      * Custom context attribute name to store the SSL session in the HTTP context. This is populated by a custom request executor.
141      */
142     private static final String CONTEXT_ATTRIBUTE_NAME_SSL_SESSION = "ssl.session";
143 
144     private static final Logger LOGGER = LoggerFactory.getLogger(ApacheTransporter.class);
145 
146     private final ChecksumExtractor checksumExtractor;
147 
148     private final PathProcessor pathProcessor;
149 
150     private final AuthenticationContext repoAuthContext;
151 
152     private final AuthenticationContext proxyAuthContext;
153 
154     private final URI baseUri;
155 
156     private final HttpHost server;
157 
158     private final HttpHost proxy;
159 
160     private final CloseableHttpClient client;
161 
162     private final Map<?, ?> headers;
163 
164     private final LocalState state;
165 
166     private final boolean preemptiveAuth;
167 
168     private final boolean preemptivePutAuth;
169 
170     private final boolean supportWebDav;
171 
172     private final boolean sendRfc9457Accept;
173 
174     private final AuthCache authCache;
175 
176     @SuppressWarnings("checkstyle:methodlength")
177     ApacheTransporter(
178             RemoteRepository repository,
179             RepositorySystemSession session,
180             ChecksumExtractor checksumExtractor,
181             PathProcessor pathProcessor)
182             throws NoTransporterException {
183         this.checksumExtractor = checksumExtractor;
184         this.pathProcessor = pathProcessor;
185         try {
186             this.baseUri = HttpTransporterUtils.getBaseUri(repository);
187             this.server = URIUtils.extractHost(baseUri);
188             if (server == null) {
189                 throw new URISyntaxException(repository.getUrl(), "URL lacks host name");
190             }
191         } catch (URISyntaxException e) {
192             throw new NoTransporterException(repository, e.getMessage(), e);
193         }
194         this.proxy = toHost(repository.getProxy());
195 
196         this.repoAuthContext = AuthenticationContext.forRepository(session, repository);
197         this.proxyAuthContext = AuthenticationContext.forProxy(session, repository);
198 
199         String httpsSecurityMode = HttpTransporterUtils.getHttpsSecurityMode(session, repository);
200         final int connectionMaxTtlSeconds = HttpTransporterUtils.getHttpConnectionMaxTtlSeconds(session, repository);
201         final int maxConnectionsPerRoute = HttpTransporterUtils.getHttpMaxConnectionsPerRoute(session, repository);
202         this.state = new LocalState(
203                 session,
204                 repository,
205                 new ConnMgrConfig(
206                         session, repoAuthContext, httpsSecurityMode, connectionMaxTtlSeconds, maxConnectionsPerRoute));
207 
208         this.headers = HttpTransporterUtils.getHttpHeaders(session, repository);
209         this.preemptiveAuth = HttpTransporterUtils.isHttpPreemptiveAuth(session, repository);
210         this.preemptivePutAuth = HttpTransporterUtils.isHttpPreemptivePutAuth(session, repository);
211         this.supportWebDav = HttpTransporterUtils.isHttpSupportWebDav(session, repository);
212         this.sendRfc9457Accept = HttpTransporterUtils.isHttpSendRfc9457Accept(session, repository);
213         int connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository);
214         int requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository);
215         int retryCount = HttpTransporterUtils.getHttpRetryHandlerCount(session, repository);
216         long retryInterval = HttpTransporterUtils.getHttpRetryHandlerInterval(session, repository);
217         long retryIntervalMax = HttpTransporterUtils.getHttpRetryHandlerIntervalMax(session, repository);
218         String retryHandlerName = ConfigUtils.getString(
219                 session,
220                 HTTP_RETRY_HANDLER_NAME_STANDARD,
221                 CONFIG_PROP_HTTP_RETRY_HANDLER_NAME + "." + repository.getId(),
222                 CONFIG_PROP_HTTP_RETRY_HANDLER_NAME);
223         boolean retryHandlerRequestSentEnabled = ConfigUtils.getBoolean(
224                 session,
225                 DEFAULT_HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED,
226                 CONFIG_PROP_HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED + "." + repository.getId(),
227                 CONFIG_PROP_HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED);
228         int maxRedirects = ConfigUtils.getInteger(
229                 session,
230                 DEFAULT_MAX_REDIRECTS,
231                 CONFIG_PROP_MAX_REDIRECTS + "." + repository.getId(),
232                 CONFIG_PROP_MAX_REDIRECTS);
233         boolean followRedirects = ConfigUtils.getBoolean(
234                 session,
235                 DEFAULT_FOLLOW_REDIRECTS,
236                 CONFIG_PROP_FOLLOW_REDIRECTS + "." + repository.getId(),
237                 CONFIG_PROP_FOLLOW_REDIRECTS);
238         boolean followInsecureRedirects = ConfigUtils.getBoolean(
239                 session,
240                 DEFAULT_FOLLOW_INSECURE_REDIRECTS,
241                 CONFIG_PROP_FOLLOW_INSECURE_REDIRECTS + "." + repository.getId(),
242                 CONFIG_PROP_FOLLOW_INSECURE_REDIRECTS);
243         String userAgent = HttpTransporterUtils.getUserAgent(session, repository);
244 
245         Charset credentialsCharset = HttpTransporterUtils.getHttpCredentialsEncoding(session, repository);
246         Registry<AuthSchemeProvider> authSchemeRegistry = RegistryBuilder.<AuthSchemeProvider>create()
247                 .register(AuthSchemes.BASIC, new BasicSchemeFactory(credentialsCharset))
248                 .register(AuthSchemes.DIGEST, new DigestSchemeFactory(credentialsCharset))
249                 .register(AuthSchemes.NTLM, new NTLMSchemeFactory())
250                 .register(AuthSchemes.SPNEGO, new SPNegoSchemeFactory())
251                 .register(AuthSchemes.KERBEROS, new KerberosSchemeFactory())
252                 .build();
253         SocketConfig socketConfig =
254                 // the time to establish connection (low level)
255                 SocketConfig.custom().setSoTimeout(requestTimeout).build();
256         RequestConfig requestConfig = RequestConfig.custom()
257                 .setMaxRedirects(maxRedirects)
258                 .setRedirectsEnabled(followRedirects)
259                 .setRelativeRedirectsAllowed(followRedirects)
260                 // the time waiting for data; max time between two data packets
261                 .setSocketTimeout(requestTimeout)
262                 // the time to establish the connection (high level)
263                 .setConnectTimeout(connectTimeout)
264                 // the time to wait for a connection from the connection manager/pool
265                 .setConnectionRequestTimeout(connectTimeout)
266                 .setLocalAddress(HttpTransporterUtils.getHttpLocalAddress(session, repository)
267                         .orElse(null))
268                 .setCookieSpec(CookieSpecs.STANDARD)
269                 .build();
270 
271         HttpRequestRetryHandler retryHandler;
272         if (HTTP_RETRY_HANDLER_NAME_STANDARD.equals(retryHandlerName)) {
273             retryHandler = new StandardHttpRequestRetryHandler(retryCount, retryHandlerRequestSentEnabled);
274         } else if (HTTP_RETRY_HANDLER_NAME_DEFAULT.equals(retryHandlerName)) {
275             retryHandler = new DefaultHttpRequestRetryHandler(retryCount, retryHandlerRequestSentEnabled);
276         } else {
277             throw new IllegalArgumentException(
278                     "Unsupported parameter " + CONFIG_PROP_HTTP_RETRY_HANDLER_NAME + " value: " + retryHandlerName);
279         }
280         ServiceUnavailableRetryStrategy serviceUnavailableRetryStrategy = new ResolverServiceUnavailableRetryStrategy(
281                 retryCount,
282                 retryInterval,
283                 retryIntervalMax,
284                 HttpTransporterUtils.getHttpServiceUnavailableCodes(session, repository));
285 
286         HttpClientBuilder builder = HttpClientBuilder.create()
287                 .setUserAgent(userAgent)
288                 .setRedirectStrategy(new ResolverRedirectStrategy(followInsecureRedirects))
289                 .setDefaultSocketConfig(socketConfig)
290                 .setDefaultRequestConfig(requestConfig)
291                 .setServiceUnavailableRetryStrategy(serviceUnavailableRetryStrategy)
292                 .setRetryHandler(retryHandler)
293                 .setDefaultAuthSchemeRegistry(authSchemeRegistry)
294                 .setConnectionManager(state.getConnectionManager())
295                 .setConnectionManagerShared(true)
296                 .setDefaultCredentialsProvider(toCredentialsProvider(server, repoAuthContext, proxy, proxyAuthContext))
297                 .setProxy(proxy);
298         if (ConfigUtils.getBoolean(
299                 session,
300                 ApacheTransporterConfigurationKeys.DEFAULT_ORIGIN_SCOPED_HEADERS,
301                 ApacheTransporterConfigurationKeys.CONFIG_PROP_ORIGIN_SCOPED_HEADERS + "." + repository.getId(),
302                 ApacheTransporterConfigurationKeys.CONFIG_PROP_ORIGIN_SCOPED_HEADERS)) {
303             // Configured headers are per-repository data and frequently carry credentials; scope them to the
304             // repository origin so a cross-origin redirect hop does not replay them to the redirect target.
305             // Challenge-based credentials are host-scoped by the credentials provider already.
306             builder.addInterceptorLast(new OriginScopedHeadersInterceptor(server, this.headers.keySet()));
307         }
308         final boolean useSystemProperties = ConfigUtils.getBoolean(
309                 session,
310                 DEFAULT_USE_SYSTEM_PROPERTIES,
311                 CONFIG_PROP_USE_SYSTEM_PROPERTIES + "." + repository.getId(),
312                 CONFIG_PROP_USE_SYSTEM_PROPERTIES);
313         if (useSystemProperties) {
314             LOGGER.warn(
315                     "Transport used Apache HttpClient is instructed to use system properties: this may yield in unwanted side-effects!");
316             LOGGER.warn("Please use documented means to configure resolver transport.");
317             builder.useSystemProperties();
318         }
319 
320         // capture SSL session for logging purposes (https://issues.apache.org/jira/browse/HTTPCLIENT-2164)
321         builder.setRequestExecutor(new HttpRequestExecutor() {
322 
323             @Override
324             public HttpResponse execute(HttpRequest request, HttpClientConnection conn, HttpContext context)
325                     throws IOException, HttpException {
326                 if (conn instanceof ManagedHttpClientConnection) {
327                     context.setAttribute(
328                             CONTEXT_ATTRIBUTE_NAME_SSL_SESSION, ((ManagedHttpClientConnection) conn).getSSLSession());
329                 }
330                 return super.execute(request, conn, context);
331             }
332         });
333 
334         HttpTransporterUtils.getHttpExpectContinue(session, repository).ifPresent(state::setExpectContinue);
335         if (!HttpTransporterUtils.isHttpReuseConnections(session, repository)) {
336             builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
337         }
338 
339         if (session.getCache() != null) {
340             this.authCache = (AuthCache) session.getCache()
341                     .computeIfAbsent(
342                             session,
343                             Keys.of(
344                                     getClass(),
345                                     repository.getId() + "-" + StringDigestUtil.sha1(repository.toString())),
346                             ConcurrentAuthCache::new);
347         } else {
348             this.authCache = new ConcurrentAuthCache();
349         }
350         this.client = builder.build();
351     }
352 
353     private static HttpHost toHost(Proxy proxy) {
354         HttpHost host = null;
355         if (proxy != null) {
356             // in Maven, the proxy.protocol is used for proxy matching against remote repository protocol; no TLS proxy
357             // support
358             // https://github.com/apache/maven/issues/2519
359             // https://github.com/apache/maven-resolver/issues/745
360             host = new HttpHost(proxy.getHost(), proxy.getPort());
361         }
362         return host;
363     }
364 
365     private static CredentialsProvider toCredentialsProvider(
366             HttpHost server, AuthenticationContext serverAuthCtx, HttpHost proxy, AuthenticationContext proxyAuthCtx) {
367         CredentialsProvider provider =
368                 toCredentialsProvider(server.getHostName(), effectivePort(server), serverAuthCtx);
369         if (proxy != null) {
370             CredentialsProvider p = toCredentialsProvider(proxy.getHostName(), proxy.getPort(), proxyAuthCtx);
371             provider = new DemuxCredentialsProvider(provider, p, proxy);
372         }
373         return provider;
374     }
375 
376     /**
377      * Determines the effective port of the given host: the explicit port if present, otherwise the default port
378      * implied by the scheme. Used to bind repository credentials to the repository's own origin (host and port)
379      * instead of {@link AuthScope#ANY_PORT}: with any-port scoping, a request landing on the same host but a
380      * different port - for example after an https-to-http downgrade redirect - would still be eligible to
381      * receive the credentials.
382      */
383     static int effectivePort(HttpHost host) {
384         if (host.getPort() >= 0) {
385             return host.getPort();
386         }
387         return "https".equalsIgnoreCase(host.getSchemeName()) ? 443 : 80;
388     }
389 
390     private static CredentialsProvider toCredentialsProvider(String host, int port, AuthenticationContext ctx) {
391         DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
392         if (ctx != null) {
393             AuthScope basicScope = new AuthScope(host, port);
394             provider.setCredentials(basicScope, new DeferredCredentialsProvider.BasicFactory(ctx));
395 
396             AuthScope ntlmScope = new AuthScope(host, port, AuthScope.ANY_REALM, "ntlm");
397             provider.setCredentials(ntlmScope, new DeferredCredentialsProvider.NtlmFactory(ctx));
398         }
399         return provider;
400     }
401 
402     LocalState getState() {
403         return state;
404     }
405 
406     private URI resolve(TransportTask task) {
407         return UriUtils.resolve(baseUri, task.getLocation());
408     }
409 
410     @Override
411     protected void implPeek(PeekTask task) throws Exception {
412         HttpHead request = commonHeaders(new HttpHead(resolve(task)));
413         try {
414             execute(request, null, task.getListener());
415         } catch (HttpResponseException e) {
416             throw new HttpTransporterException(e.getStatusCode());
417         }
418     }
419 
420     @Override
421     protected void implGet(GetTask task) throws Exception {
422         boolean resume = true;
423 
424         EntityGetter getter = new EntityGetter(task);
425         HttpGet request = commonHeaders(new HttpGet(resolve(task)));
426         if (sendRfc9457Accept) {
427             ApacheRFC9457Reporter.INSTANCE.prepareRequest(request);
428         }
429         while (true) {
430             try {
431                 if (resume) {
432                     resume(request, task);
433                 }
434                 execute(request, getter, task.getListener());
435                 break;
436             } catch (HttpResponseException e) {
437                 if (resume
438                         && e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED
439                         && request.containsHeader(HttpHeaders.RANGE)) {
440                     request = commonHeaders(new HttpGet(resolve(task)));
441                     resume = false;
442                     continue;
443                 }
444                 throw new HttpTransporterException(e.getStatusCode());
445             }
446         }
447     }
448 
449     @Override
450     protected void implPut(PutTask task) throws Exception {
451         PutTaskEntity entity = new PutTaskEntity(task);
452         HttpPut request = commonHeaders(entity(new HttpPut(resolve(task)), entity));
453         if (sendRfc9457Accept) {
454             ApacheRFC9457Reporter.INSTANCE.prepareRequest(request);
455         }
456         try {
457             execute(request, null, task.getListener());
458         } catch (HttpResponseException e) {
459             if (e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader(HttpHeaders.EXPECT)) {
460                 state.setExpectContinue(false);
461                 request = commonHeaders(entity(new HttpPut(request.getURI()), entity));
462                 execute(request, null, task.getListener());
463                 return;
464             }
465             throw new HttpTransporterException(e.getStatusCode());
466         }
467     }
468 
469     private void execute(HttpUriRequest request, EntityGetter getter, TransportListener listener) throws Exception {
470         try {
471             SharingHttpContext context = new SharingHttpContext(state);
472             context.setAuthCache(authCache);
473             prepare(request, context);
474             try (CloseableHttpResponse response = client.execute(server, request, context)) {
475                 try {
476                     Map<TransferEvent.TransportPropertyKey, Object> transportProperties =
477                             createTransportProperties(response, context);
478                     listener.transportPropertiesAvailable(transportProperties);
479                     handleStatus(response);
480                     if (getter != null) {
481                         getter.handle(response);
482                     }
483                 } finally {
484                     EntityUtils.consumeQuietly(response.getEntity());
485                 }
486             }
487         } catch (IOException e) {
488             if (e.getCause() instanceof TransferCancelledException) {
489                 throw (Exception) e.getCause();
490             }
491             throw e;
492         }
493     }
494 
495     private void prepare(HttpUriRequest request, SharingHttpContext context) throws Exception {
496         final boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase(request.getMethod());
497         if (preemptiveAuth || (preemptivePutAuth && put)) {
498             context.getAuthCache().put(server, new BasicScheme());
499         }
500         if (supportWebDav) {
501             if (state.getWebDav() == null && (put || isPayloadPresent(request))) {
502                 HttpOptions req = commonHeaders(new HttpOptions(request.getURI()));
503                 try (CloseableHttpResponse response = client.execute(server, req, context)) {
504                     state.setWebDav(response.containsHeader(HttpHeaders.DAV));
505                     EntityUtils.consumeQuietly(response.getEntity());
506                 } catch (IOException e) {
507                     LOGGER.debug("Failed to prepare HTTP context", e);
508                 }
509             }
510             if (put && Boolean.TRUE.equals(state.getWebDav())) {
511                 mkdirs(request.getURI(), context);
512             }
513         }
514     }
515 
516     private void mkdirs(URI uri, SharingHttpContext context) throws Exception {
517         List<URI> dirs = UriUtils.getDirectories(baseUri, uri);
518         int index = 0;
519         for (; index < dirs.size(); index++) {
520             try (CloseableHttpResponse response =
521                     client.execute(server, commonHeaders(new HttpMkCol(dirs.get(index))), context)) {
522                 try {
523                     int status = response.getStatusLine().getStatusCode();
524                     if (status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
525                         break;
526                     } else if (status == HttpStatus.SC_CONFLICT) {
527                         continue;
528                     }
529                     handleStatus(response);
530                 } finally {
531                     EntityUtils.consumeQuietly(response.getEntity());
532                 }
533             } catch (IOException e) {
534                 LOGGER.debug("Failed to create parent directory {}", dirs.get(index), e);
535                 return;
536             }
537         }
538         for (index--; index >= 0; index--) {
539             try (CloseableHttpResponse response =
540                     client.execute(server, commonHeaders(new HttpMkCol(dirs.get(index))), context)) {
541                 try {
542                     handleStatus(response);
543                 } finally {
544                     EntityUtils.consumeQuietly(response.getEntity());
545                 }
546             } catch (IOException e) {
547                 LOGGER.debug("Failed to create parent directory {}", dirs.get(index), e);
548                 return;
549             }
550         }
551     }
552 
553     private <T extends HttpEntityEnclosingRequest> T entity(T request, HttpEntity entity) {
554         request.setEntity(entity);
555         return request;
556     }
557 
558     private boolean isPayloadPresent(HttpUriRequest request) {
559         if (request instanceof HttpEntityEnclosingRequest) {
560             HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
561             return entity != null && entity.getContentLength() != 0;
562         }
563         return false;
564     }
565 
566     private <T extends HttpUriRequest> T commonHeaders(T request) {
567         request.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store");
568         request.setHeader(HttpHeaders.PRAGMA, "no-cache");
569 
570         if (state.isExpectContinue() && isPayloadPresent(request)) {
571             request.setHeader(HttpHeaders.EXPECT, "100-continue");
572         }
573 
574         for (Map.Entry<?, ?> entry : headers.entrySet()) {
575             if (!(entry.getKey() instanceof String)) {
576                 continue;
577             }
578             if (entry.getValue() instanceof String) {
579                 request.setHeader(entry.getKey().toString(), entry.getValue().toString());
580             } else {
581                 request.removeHeaders(entry.getKey().toString());
582             }
583         }
584 
585         if (!state.isExpectContinue()) {
586             request.removeHeaders(HttpHeaders.EXPECT);
587         }
588         return request;
589     }
590 
591     private <T extends HttpUriRequest> void resume(T request, GetTask task) throws IOException {
592         long resumeOffset = task.getResumeOffset();
593         if (resumeOffset > 0L && task.getDataPath() != null) {
594             long lastModified = Files.getLastModifiedTime(task.getDataPath()).toMillis();
595             request.setHeader(HttpHeaders.RANGE, "bytes=" + resumeOffset + '-');
596             request.setHeader(
597                     HttpHeaders.IF_UNMODIFIED_SINCE, DateUtils.formatDate(new Date(lastModified - 60L * 1000L)));
598             request.setHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
599         }
600     }
601 
602     private void handleStatus(CloseableHttpResponse response) throws Exception {
603         int status = response.getStatusLine().getStatusCode();
604         if (status >= 300) {
605             ApacheRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
606                 throw new HttpResponseException(statusCode, reasonPhrase + " (" + statusCode + ")");
607             });
608         }
609     }
610 
611     @Override
612     protected void implClose() {
613         try {
614             client.close();
615         } catch (IOException e) {
616             throw new UncheckedIOException(e);
617         }
618         AuthenticationContext.close(repoAuthContext);
619         AuthenticationContext.close(proxyAuthContext);
620         state.close();
621     }
622 
623     private class EntityGetter {
624 
625         private final GetTask task;
626 
627         EntityGetter(GetTask task) {
628             this.task = task;
629         }
630 
631         public void handle(CloseableHttpResponse response) throws IOException, TransferCancelledException {
632             HttpEntity entity = response.getEntity();
633             if (entity == null) {
634                 entity = new ByteArrayEntity(new byte[0]);
635             }
636 
637             long offset = 0L, length = entity.getContentLength();
638             Header rangeHeader = response.getFirstHeader(HttpHeaders.CONTENT_RANGE);
639             String range = rangeHeader != null ? rangeHeader.getValue() : null;
640             if (range != null) {
641                 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
642                 if (!m.matches()) {
643                     throw new IOException("Invalid Content-Range header for partial download: " + range);
644                 }
645                 offset = Long.parseLong(m.group(1));
646                 length = Long.parseLong(m.group(2)) + 1L;
647                 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
648                     throw new IOException("Invalid Content-Range header for partial download from offset "
649                             + task.getResumeOffset() + ": " + range);
650                 }
651             }
652 
653             final boolean resume = offset > 0L;
654             final Path dataFile = task.getDataPath();
655             if (dataFile == null) {
656                 try (InputStream is = entity.getContent()) {
657                     utilGet(task, is, true, length, resume);
658                     extractChecksums(response);
659                 }
660             } else {
661                 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
662                     task.setDataPath(tempFile.getPath(), resume);
663                     if (resume && Files.isRegularFile(dataFile)) {
664                         try (InputStream inputStream = Files.newInputStream(dataFile)) {
665                             Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
666                         }
667                     }
668                     try (InputStream is = entity.getContent()) {
669                         utilGet(task, is, true, length, resume);
670                     }
671                     tempFile.move();
672                 } finally {
673                     task.setDataPath(dataFile);
674                 }
675             }
676             if (task.getDataPath() != null) {
677                 Header lastModifiedHeader =
678                         response.getFirstHeader(HttpHeaders.LAST_MODIFIED); // note: Wagon also does first not last
679                 if (lastModifiedHeader != null) {
680                     Date lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
681                     if (lastModified != null) {
682                         pathProcessor.setLastModified(
683                                 task.getDataPath(),
684                                 HttpTransporterUtils.clampRemoteLastModified(lastModified.getTime()));
685                     }
686                 }
687             }
688             extractChecksums(response);
689         }
690 
691         private void extractChecksums(CloseableHttpResponse response) {
692             Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
693             if (checksums != null && !checksums.isEmpty()) {
694                 checksums.forEach(task::setChecksum);
695             }
696         }
697     }
698 
699     private static Map<TransferEvent.TransportPropertyKey, Object> createTransportProperties(
700             CloseableHttpResponse response, HttpCoreContext context) {
701         HttpTransportPropertiesBuilder builder =
702                 new HttpTransportPropertiesBuilder(toHttpVersion(response.getProtocolVersion()));
703         SSLSession sslSession = context.getAttribute(CONTEXT_ATTRIBUTE_NAME_SSL_SESSION, SSLSession.class);
704         if (sslSession != null) {
705             builder.withSslProtocol(sslSession.getProtocol());
706             builder.withSslCipherSuite(sslSession.getCipherSuite());
707         }
708         // content encoding is not available (see https://issues.apache.org/jira/browse/HTTPCORE-792)
709         return builder.build();
710     }
711 
712     static HttpVersion toHttpVersion(ProtocolVersion version) {
713         switch (version.getMajor()) {
714             case 1:
715                 if (version.getMinor() == 0) {
716                     return HttpVersion.HTTP_1_0;
717                 } else {
718                     return HttpVersion.HTTP_1_1;
719                 }
720             case 2:
721                 return HttpVersion.HTTP_2;
722             case 3:
723                 return HttpVersion.HTTP_3;
724             default:
725                 throw new IllegalArgumentException("Unknown version " + version.toString());
726         }
727     }
728 
729     private static Function<String, String> headerGetter(CloseableHttpResponse closeableHttpResponse) {
730         return s -> {
731             Header header = closeableHttpResponse.getFirstHeader(s);
732             return header != null ? header.getValue() : null;
733         };
734     }
735 
736     private class PutTaskEntity extends AbstractHttpEntity {
737 
738         private final PutTask task;
739 
740         PutTaskEntity(PutTask task) {
741             this.task = task;
742         }
743 
744         @Override
745         public boolean isRepeatable() {
746             return true;
747         }
748 
749         @Override
750         public boolean isStreaming() {
751             return false;
752         }
753 
754         @Override
755         public long getContentLength() {
756             return task.getDataLength();
757         }
758 
759         @Override
760         public InputStream getContent() throws IOException {
761             return task.newInputStream();
762         }
763 
764         @Override
765         public void writeTo(OutputStream os) throws IOException {
766             try {
767                 utilPut(task, os, false);
768             } catch (TransferCancelledException e) {
769                 throw (IOException) new InterruptedIOException().initCause(e);
770             }
771         }
772     }
773 
774     private static class ResolverServiceUnavailableRetryStrategy implements ServiceUnavailableRetryStrategy {
775         private final int retryCount;
776 
777         private final long retryInterval;
778 
779         private final long retryIntervalMax;
780 
781         private final Set<Integer> serviceUnavailableHttpCodes;
782 
783         /**
784          * Ugly, but forced by HttpClient API {@link ServiceUnavailableRetryStrategy}: the calls for
785          * {@link #retryRequest(HttpResponse, int, HttpContext)} and {@link #getRetryInterval()} are done by same
786          * thread and are actually done from spot that are very close to each other (almost subsequent calls).
787          */
788         private static final ThreadLocal<Long> RETRY_INTERVAL_HOLDER = new ThreadLocal<>();
789 
790         private ResolverServiceUnavailableRetryStrategy(
791                 int retryCount, long retryInterval, long retryIntervalMax, Set<Integer> serviceUnavailableHttpCodes) {
792             if (retryCount < 0) {
793                 throw new IllegalArgumentException("retryCount must be >= 0");
794             }
795             if (retryInterval < 0L) {
796                 throw new IllegalArgumentException("retryInterval must be >= 0");
797             }
798             if (retryIntervalMax < 0L) {
799                 throw new IllegalArgumentException("retryIntervalMax must be >= 0");
800             }
801             this.retryCount = retryCount;
802             this.retryInterval = retryInterval;
803             this.retryIntervalMax = retryIntervalMax;
804             this.serviceUnavailableHttpCodes = requireNonNull(serviceUnavailableHttpCodes);
805         }
806 
807         @Override
808         public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
809             final boolean retry = executionCount <= retryCount
810                     && (serviceUnavailableHttpCodes.contains(
811                             response.getStatusLine().getStatusCode()));
812             if (retry) {
813                 Long retryInterval = retryInterval(response, executionCount, context);
814                 if (retryInterval != null) {
815                     RETRY_INTERVAL_HOLDER.set(retryInterval);
816                     return true;
817                 }
818             }
819             RETRY_INTERVAL_HOLDER.remove();
820             return false;
821         }
822 
823         /**
824          * Calculates retry interval in milliseconds. If {@link HttpHeaders#RETRY_AFTER} header present, it obeys it.
825          * Otherwise, it returns {@link this#retryInterval} long value multiplied with {@code executionCount} (starts
826          * from 1 and goes 2, 3,...).
827          *
828          * @return Long representing the retry interval as millis, or {@code null} if the request should be failed.
829          */
830         private Long retryInterval(HttpResponse httpResponse, int executionCount, HttpContext httpContext) {
831             Long result = null;
832             Header header = httpResponse.getFirstHeader(HttpHeaders.RETRY_AFTER);
833             if (header != null && header.getValue() != null) {
834                 String headerValue = header.getValue();
835                 if (headerValue.contains(":")) { // is date when to retry
836                     Date when = DateUtils.parseDate(headerValue); // presumably future
837                     if (when != null) {
838                         result = Math.max(when.getTime() - System.currentTimeMillis(), 0L);
839                     }
840                 } else {
841                     try {
842                         result = Long.parseLong(headerValue) * 1000L; // is in seconds
843                     } catch (NumberFormatException e) {
844                         // fall through
845                     }
846                 }
847             }
848             if (result == null) {
849                 result = executionCount * this.retryInterval;
850             }
851             if (result > retryIntervalMax) {
852                 return null;
853             }
854             return result;
855         }
856 
857         @Override
858         public long getRetryInterval() {
859             Long ri = RETRY_INTERVAL_HOLDER.get();
860             if (ri == null) {
861                 return 0L;
862             }
863             RETRY_INTERVAL_HOLDER.remove();
864             return ri;
865         }
866     }
867 
868     static class ConcurrentAuthCache implements AuthCache {
869         private final ConcurrentHashMap<HttpHost, AuthScheme> map = new ConcurrentHashMap<>();
870 
871         @Override
872         public void put(HttpHost host, AuthScheme authScheme) {
873             if (host != null && authScheme != null) {
874                 map.put(host, authScheme);
875             }
876         }
877 
878         @Override
879         public AuthScheme get(HttpHost host) {
880             if (host == null) {
881                 return null;
882             }
883             return map.get(host);
884         }
885 
886         @Override
887         public void remove(HttpHost host) {
888             if (host != null) {
889                 map.remove(host);
890             }
891         }
892 
893         @Override
894         public void clear() {
895             map.clear();
896         }
897     }
898 }