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.http;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.InterruptedIOException;
25  import java.io.OutputStream;
26  import java.io.UncheckedIOException;
27  import java.net.InetAddress;
28  import java.net.URI;
29  import java.net.URISyntaxException;
30  import java.net.UnknownHostException;
31  import java.nio.charset.Charset;
32  import java.nio.file.Files;
33  import java.nio.file.StandardCopyOption;
34  import java.util.Collections;
35  import java.util.Date;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  import org.apache.http.Header;
42  import org.apache.http.HttpEntity;
43  import org.apache.http.HttpEntityEnclosingRequest;
44  import org.apache.http.HttpHeaders;
45  import org.apache.http.HttpHost;
46  import org.apache.http.HttpStatus;
47  import org.apache.http.auth.AuthSchemeProvider;
48  import org.apache.http.auth.AuthScope;
49  import org.apache.http.client.CredentialsProvider;
50  import org.apache.http.client.HttpRequestRetryHandler;
51  import org.apache.http.client.HttpResponseException;
52  import org.apache.http.client.config.AuthSchemes;
53  import org.apache.http.client.config.RequestConfig;
54  import org.apache.http.client.methods.CloseableHttpResponse;
55  import org.apache.http.client.methods.HttpGet;
56  import org.apache.http.client.methods.HttpHead;
57  import org.apache.http.client.methods.HttpOptions;
58  import org.apache.http.client.methods.HttpPut;
59  import org.apache.http.client.methods.HttpUriRequest;
60  import org.apache.http.client.utils.DateUtils;
61  import org.apache.http.client.utils.URIUtils;
62  import org.apache.http.config.Registry;
63  import org.apache.http.config.RegistryBuilder;
64  import org.apache.http.config.SocketConfig;
65  import org.apache.http.entity.AbstractHttpEntity;
66  import org.apache.http.entity.ByteArrayEntity;
67  import org.apache.http.impl.NoConnectionReuseStrategy;
68  import org.apache.http.impl.auth.BasicScheme;
69  import org.apache.http.impl.auth.BasicSchemeFactory;
70  import org.apache.http.impl.auth.DigestSchemeFactory;
71  import org.apache.http.impl.auth.KerberosSchemeFactory;
72  import org.apache.http.impl.auth.NTLMSchemeFactory;
73  import org.apache.http.impl.auth.SPNegoSchemeFactory;
74  import org.apache.http.impl.client.CloseableHttpClient;
75  import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
76  import org.apache.http.impl.client.HttpClientBuilder;
77  import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
78  import org.apache.http.util.EntityUtils;
79  import org.eclipse.aether.ConfigurationProperties;
80  import org.eclipse.aether.RepositorySystemSession;
81  import org.eclipse.aether.repository.AuthenticationContext;
82  import org.eclipse.aether.repository.Proxy;
83  import org.eclipse.aether.repository.RemoteRepository;
84  import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
85  import org.eclipse.aether.spi.connector.transport.GetTask;
86  import org.eclipse.aether.spi.connector.transport.PeekTask;
87  import org.eclipse.aether.spi.connector.transport.PutTask;
88  import org.eclipse.aether.spi.connector.transport.TransportTask;
89  import org.eclipse.aether.transfer.NoTransporterException;
90  import org.eclipse.aether.transfer.TransferCancelledException;
91  import org.eclipse.aether.util.ConfigUtils;
92  import org.eclipse.aether.util.FileUtils;
93  import org.slf4j.Logger;
94  import org.slf4j.LoggerFactory;
95  
96  import static java.util.Objects.requireNonNull;
97  
98  /**
99   * A transporter for HTTP/HTTPS.
100  */
101 final class HttpTransporter extends AbstractTransporter {
102 
103     static final String BIND_ADDRESS = "aether.connector.bind.address";
104 
105     static final String SUPPORT_WEBDAV = "aether.connector.http.supportWebDav";
106 
107     static final String PREEMPTIVE_PUT_AUTH = "aether.connector.http.preemptivePutAuth";
108 
109     static final String USE_SYSTEM_PROPERTIES = "aether.connector.http.useSystemProperties";
110 
111     static final String HTTP_RETRY_HANDLER_NAME = "aether.connector.http.retryHandler.name";
112 
113     private static final String HTTP_RETRY_HANDLER_NAME_STANDARD = "standard";
114 
115     private static final String HTTP_RETRY_HANDLER_NAME_DEFAULT = "default";
116 
117     static final String HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED =
118             "aether.connector.http.retryHandler.requestSentEnabled";
119 
120     private static final Pattern CONTENT_RANGE_PATTERN =
121             Pattern.compile("\\s*bytes\\s+([0-9]+)\\s*-\\s*([0-9]+)\\s*/.*");
122 
123     private static final Logger LOGGER = LoggerFactory.getLogger(HttpTransporter.class);
124 
125     private final Map<String, ChecksumExtractor> checksumExtractors;
126 
127     private final AuthenticationContext repoAuthContext;
128 
129     private final AuthenticationContext proxyAuthContext;
130 
131     private final URI baseUri;
132 
133     private final HttpHost server;
134 
135     private final HttpHost proxy;
136 
137     private final CloseableHttpClient client;
138 
139     private final Map<?, ?> headers;
140 
141     private final LocalState state;
142 
143     private final boolean preemptiveAuth;
144 
145     private final boolean preemptivePutAuth;
146 
147     private final boolean supportWebDav;
148 
149     HttpTransporter(
150             Map<String, ChecksumExtractor> checksumExtractors,
151             RemoteRepository repository,
152             RepositorySystemSession session)
153             throws NoTransporterException {
154         if (!"http".equalsIgnoreCase(repository.getProtocol()) && !"https".equalsIgnoreCase(repository.getProtocol())) {
155             throw new NoTransporterException(repository);
156         }
157         this.checksumExtractors = requireNonNull(checksumExtractors, "checksum extractors must not be null");
158         try {
159             this.baseUri = new URI(repository.getUrl()).parseServerAuthority();
160             if (baseUri.isOpaque()) {
161                 throw new URISyntaxException(repository.getUrl(), "URL must not be opaque");
162             }
163             this.server = URIUtils.extractHost(baseUri);
164             if (server == null) {
165                 throw new URISyntaxException(repository.getUrl(), "URL lacks host name");
166             }
167         } catch (URISyntaxException e) {
168             throw new NoTransporterException(repository, e.getMessage(), e);
169         }
170         this.proxy = toHost(repository.getProxy());
171 
172         this.repoAuthContext = AuthenticationContext.forRepository(session, repository);
173         this.proxyAuthContext = AuthenticationContext.forProxy(session, repository);
174 
175         String httpsSecurityMode = ConfigUtils.getString(
176                 session,
177                 ConfigurationProperties.HTTPS_SECURITY_MODE_DEFAULT,
178                 ConfigurationProperties.HTTPS_SECURITY_MODE + "." + repository.getId(),
179                 ConfigurationProperties.HTTPS_SECURITY_MODE);
180         final int connectionMaxTtlSeconds = ConfigUtils.getInteger(
181                 session,
182                 ConfigurationProperties.DEFAULT_HTTP_CONNECTION_MAX_TTL,
183                 ConfigurationProperties.HTTP_CONNECTION_MAX_TTL + "." + repository.getId(),
184                 ConfigurationProperties.HTTP_CONNECTION_MAX_TTL);
185         final int maxConnectionsPerRoute = ConfigUtils.getInteger(
186                 session,
187                 ConfigurationProperties.DEFAULT_HTTP_MAX_CONNECTIONS_PER_ROUTE,
188                 ConfigurationProperties.HTTP_MAX_CONNECTIONS_PER_ROUTE + "." + repository.getId(),
189                 ConfigurationProperties.HTTP_MAX_CONNECTIONS_PER_ROUTE);
190         this.state = new LocalState(
191                 session,
192                 repository,
193                 new ConnMgrConfig(
194                         session, repoAuthContext, httpsSecurityMode, connectionMaxTtlSeconds, maxConnectionsPerRoute));
195 
196         this.headers = ConfigUtils.getMap(
197                 session,
198                 Collections.emptyMap(),
199                 ConfigurationProperties.HTTP_HEADERS + "." + repository.getId(),
200                 ConfigurationProperties.HTTP_HEADERS);
201 
202         this.preemptiveAuth = ConfigUtils.getBoolean(
203                 session,
204                 ConfigurationProperties.DEFAULT_HTTP_PREEMPTIVE_AUTH,
205                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH + "." + repository.getId(),
206                 ConfigurationProperties.HTTP_PREEMPTIVE_AUTH);
207         this.preemptivePutAuth = // defaults to true: Wagon does same
208                 ConfigUtils.getBoolean(
209                         session, true, PREEMPTIVE_PUT_AUTH + "." + repository.getId(), PREEMPTIVE_PUT_AUTH);
210         this.supportWebDav = // defaults to false: who needs it will enable it
211                 ConfigUtils.getBoolean(session, false, SUPPORT_WEBDAV + "." + repository.getId(), SUPPORT_WEBDAV);
212         String credentialEncoding = ConfigUtils.getString(
213                 session,
214                 ConfigurationProperties.DEFAULT_HTTP_CREDENTIAL_ENCODING,
215                 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING + "." + repository.getId(),
216                 ConfigurationProperties.HTTP_CREDENTIAL_ENCODING);
217         int connectTimeout = ConfigUtils.getInteger(
218                 session,
219                 ConfigurationProperties.DEFAULT_CONNECT_TIMEOUT,
220                 ConfigurationProperties.CONNECT_TIMEOUT + "." + repository.getId(),
221                 ConfigurationProperties.CONNECT_TIMEOUT);
222         int requestTimeout = ConfigUtils.getInteger(
223                 session,
224                 ConfigurationProperties.DEFAULT_REQUEST_TIMEOUT,
225                 ConfigurationProperties.REQUEST_TIMEOUT + "." + repository.getId(),
226                 ConfigurationProperties.REQUEST_TIMEOUT);
227         int retryCount = ConfigUtils.getInteger(
228                 session,
229                 ConfigurationProperties.DEFAULT_HTTP_RETRY_HANDLER_COUNT,
230                 ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT + "." + repository.getId(),
231                 ConfigurationProperties.HTTP_RETRY_HANDLER_COUNT);
232         String retryHandlerName = ConfigUtils.getString(
233                 session,
234                 HTTP_RETRY_HANDLER_NAME_STANDARD,
235                 HTTP_RETRY_HANDLER_NAME + "." + repository.getId(),
236                 HTTP_RETRY_HANDLER_NAME);
237         boolean retryHandlerRequestSentEnabled = ConfigUtils.getBoolean(
238                 session,
239                 false,
240                 HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED + "." + repository.getId(),
241                 HTTP_RETRY_HANDLER_REQUEST_SENT_ENABLED);
242         String userAgent = ConfigUtils.getString(
243                 session, ConfigurationProperties.DEFAULT_USER_AGENT, ConfigurationProperties.USER_AGENT);
244 
245         Charset credentialsCharset = Charset.forName(credentialEncoding);
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                 SocketConfig.custom().setSoTimeout(requestTimeout).build();
255         RequestConfig requestConfig = RequestConfig.custom()
256                 .setConnectTimeout(connectTimeout)
257                 .setConnectionRequestTimeout(connectTimeout)
258                 .setLocalAddress(getBindAddress(session, repository))
259                 .setSocketTimeout(requestTimeout)
260                 .build();
261 
262         HttpRequestRetryHandler retryHandler;
263         if (HTTP_RETRY_HANDLER_NAME_STANDARD.equals(retryHandlerName)) {
264             retryHandler = new StandardHttpRequestRetryHandler(retryCount, retryHandlerRequestSentEnabled);
265         } else if (HTTP_RETRY_HANDLER_NAME_DEFAULT.equals(retryHandlerName)) {
266             retryHandler = new DefaultHttpRequestRetryHandler(retryCount, retryHandlerRequestSentEnabled);
267         } else {
268             throw new IllegalArgumentException(
269                     "Unsupported parameter " + HTTP_RETRY_HANDLER_NAME + " value: " + retryHandlerName);
270         }
271 
272         HttpClientBuilder builder = HttpClientBuilder.create()
273                 .setUserAgent(userAgent)
274                 .setDefaultSocketConfig(socketConfig)
275                 .setDefaultRequestConfig(requestConfig)
276                 .setRetryHandler(retryHandler)
277                 .setDefaultAuthSchemeRegistry(authSchemeRegistry)
278                 .setConnectionManager(state.getConnectionManager())
279                 .setConnectionManagerShared(true)
280                 .setDefaultCredentialsProvider(toCredentialsProvider(server, repoAuthContext, proxy, proxyAuthContext))
281                 .setProxy(proxy);
282         final boolean useSystemProperties = ConfigUtils.getBoolean(
283                 session, false, USE_SYSTEM_PROPERTIES + "." + repository.getId(), USE_SYSTEM_PROPERTIES);
284         if (useSystemProperties) {
285             LOGGER.warn(
286                     "Transport used Apache HttpClient is instructed to use system properties: this may yield in unwanted side-effects!");
287             LOGGER.warn("Please use documented means to configure resolver transport.");
288             builder.useSystemProperties();
289         }
290 
291         final boolean reuseConnections = ConfigUtils.getBoolean(
292                 session,
293                 ConfigurationProperties.DEFAULT_HTTP_REUSE_CONNECTIONS,
294                 ConfigurationProperties.HTTP_REUSE_CONNECTIONS + "." + repository.getId(),
295                 ConfigurationProperties.HTTP_REUSE_CONNECTIONS);
296         if (!reuseConnections) {
297             builder.setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE);
298         }
299 
300         this.client = builder.build();
301     }
302 
303     /**
304      * Returns non-null {@link InetAddress} if set in configuration, {@code null} otherwise.
305      */
306     private InetAddress getBindAddress(RepositorySystemSession session, RemoteRepository repository) {
307         String bindAddress =
308                 ConfigUtils.getString(session, null, BIND_ADDRESS + "." + repository.getId(), BIND_ADDRESS);
309         if (bindAddress == null) {
310             return null;
311         }
312         try {
313             return InetAddress.getByName(bindAddress);
314         } catch (UnknownHostException uhe) {
315             throw new IllegalArgumentException(
316                     "Given bind address (" + bindAddress + ") cannot be resolved for remote repository " + repository,
317                     uhe);
318         }
319     }
320 
321     private static HttpHost toHost(Proxy proxy) {
322         HttpHost host = null;
323         if (proxy != null) {
324             host = new HttpHost(proxy.getHost(), proxy.getPort());
325         }
326         return host;
327     }
328 
329     private static CredentialsProvider toCredentialsProvider(
330             HttpHost server, AuthenticationContext serverAuthCtx, HttpHost proxy, AuthenticationContext proxyAuthCtx) {
331         CredentialsProvider provider = toCredentialsProvider(server.getHostName(), AuthScope.ANY_PORT, serverAuthCtx);
332         if (proxy != null) {
333             CredentialsProvider p = toCredentialsProvider(proxy.getHostName(), proxy.getPort(), proxyAuthCtx);
334             provider = new DemuxCredentialsProvider(provider, p, proxy);
335         }
336         return provider;
337     }
338 
339     private static CredentialsProvider toCredentialsProvider(String host, int port, AuthenticationContext ctx) {
340         DeferredCredentialsProvider provider = new DeferredCredentialsProvider();
341         if (ctx != null) {
342             AuthScope basicScope = new AuthScope(host, port);
343             provider.setCredentials(basicScope, new DeferredCredentialsProvider.BasicFactory(ctx));
344 
345             AuthScope ntlmScope = new AuthScope(host, port, AuthScope.ANY_REALM, "ntlm");
346             provider.setCredentials(ntlmScope, new DeferredCredentialsProvider.NtlmFactory(ctx));
347         }
348         return provider;
349     }
350 
351     LocalState getState() {
352         return state;
353     }
354 
355     private URI resolve(TransportTask task) {
356         return UriUtils.resolve(baseUri, task.getLocation());
357     }
358 
359     @Override
360     public int classify(Throwable error) {
361         if (error instanceof HttpResponseException
362                 && ((HttpResponseException) error).getStatusCode() == HttpStatus.SC_NOT_FOUND) {
363             return ERROR_NOT_FOUND;
364         }
365         return ERROR_OTHER;
366     }
367 
368     @Override
369     protected void implPeek(PeekTask task) throws Exception {
370         HttpHead request = commonHeaders(new HttpHead(resolve(task)));
371         execute(request, null);
372     }
373 
374     @Override
375     protected void implGet(GetTask task) throws Exception {
376         boolean resume = true;
377         boolean applyChecksumExtractors = true;
378 
379         EntityGetter getter = new EntityGetter(task);
380         HttpGet request = commonHeaders(new HttpGet(resolve(task)));
381         while (true) {
382             try {
383                 if (resume) {
384                     resume(request, task);
385                 }
386                 if (applyChecksumExtractors) {
387                     for (ChecksumExtractor checksumExtractor : checksumExtractors.values()) {
388                         checksumExtractor.prepareRequest(request);
389                     }
390                 }
391                 execute(request, getter);
392                 break;
393             } catch (HttpResponseException e) {
394                 if (resume
395                         && e.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED
396                         && request.containsHeader(HttpHeaders.RANGE)) {
397                     request = commonHeaders(new HttpGet(resolve(task)));
398                     resume = false;
399                     continue;
400                 }
401                 if (applyChecksumExtractors) {
402                     boolean retryWithoutExtractors = false;
403                     for (ChecksumExtractor checksumExtractor : checksumExtractors.values()) {
404                         if (checksumExtractor.retryWithoutExtractor(e)) {
405                             retryWithoutExtractors = true;
406                             break;
407                         }
408                     }
409                     if (retryWithoutExtractors) {
410                         request = commonHeaders(new HttpGet(resolve(task)));
411                         applyChecksumExtractors = false;
412                         continue;
413                     }
414                 }
415                 throw e;
416             }
417         }
418     }
419 
420     @Override
421     protected void implPut(PutTask task) throws Exception {
422         PutTaskEntity entity = new PutTaskEntity(task);
423         HttpPut request = commonHeaders(entity(new HttpPut(resolve(task)), entity));
424         try {
425             execute(request, null);
426         } catch (HttpResponseException e) {
427             if (e.getStatusCode() == HttpStatus.SC_EXPECTATION_FAILED && request.containsHeader(HttpHeaders.EXPECT)) {
428                 state.setExpectContinue(false);
429                 request = commonHeaders(entity(new HttpPut(request.getURI()), entity));
430                 execute(request, null);
431                 return;
432             }
433             throw e;
434         }
435     }
436 
437     private void execute(HttpUriRequest request, EntityGetter getter) throws Exception {
438         try {
439             SharingHttpContext context = new SharingHttpContext(state);
440             prepare(request, context);
441             try (CloseableHttpResponse response = client.execute(server, request, context)) {
442                 try {
443                     context.close();
444                     handleStatus(response);
445                     if (getter != null) {
446                         getter.handle(response);
447                     }
448                 } finally {
449                     EntityUtils.consumeQuietly(response.getEntity());
450                 }
451             }
452         } catch (IOException e) {
453             if (e.getCause() instanceof TransferCancelledException) {
454                 throw (Exception) e.getCause();
455             }
456             throw e;
457         }
458     }
459 
460     private void prepare(HttpUriRequest request, SharingHttpContext context) {
461         final boolean put = HttpPut.METHOD_NAME.equalsIgnoreCase(request.getMethod());
462         if (preemptiveAuth || (preemptivePutAuth && put)) {
463             context.getAuthCache().put(server, new BasicScheme());
464         }
465         if (supportWebDav) {
466             if (state.getWebDav() == null && (put || isPayloadPresent(request))) {
467                 HttpOptions req = commonHeaders(new HttpOptions(request.getURI()));
468                 try (CloseableHttpResponse response = client.execute(server, req, context)) {
469                     state.setWebDav(response.containsHeader(HttpHeaders.DAV));
470                     EntityUtils.consumeQuietly(response.getEntity());
471                 } catch (IOException e) {
472                     LOGGER.debug("Failed to prepare HTTP context", e);
473                 }
474             }
475             if (put && Boolean.TRUE.equals(state.getWebDav())) {
476                 mkdirs(request.getURI(), context);
477             }
478         }
479     }
480 
481     @SuppressWarnings("checkstyle:magicnumber")
482     private void mkdirs(URI uri, SharingHttpContext context) {
483         List<URI> dirs = UriUtils.getDirectories(baseUri, uri);
484         int index = 0;
485         for (; index < dirs.size(); index++) {
486             try (CloseableHttpResponse response =
487                     client.execute(server, commonHeaders(new HttpMkCol(dirs.get(index))), context)) {
488                 try {
489                     int status = response.getStatusLine().getStatusCode();
490                     if (status < 300 || status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
491                         break;
492                     } else if (status == HttpStatus.SC_CONFLICT) {
493                         continue;
494                     }
495                     handleStatus(response);
496                 } finally {
497                     EntityUtils.consumeQuietly(response.getEntity());
498                 }
499             } catch (IOException e) {
500                 LOGGER.debug("Failed to create parent directory {}", dirs.get(index), e);
501                 return;
502             }
503         }
504         for (index--; index >= 0; index--) {
505             try (CloseableHttpResponse response =
506                     client.execute(server, commonHeaders(new HttpMkCol(dirs.get(index))), context)) {
507                 try {
508                     handleStatus(response);
509                 } finally {
510                     EntityUtils.consumeQuietly(response.getEntity());
511                 }
512             } catch (IOException e) {
513                 LOGGER.debug("Failed to create parent directory {}", dirs.get(index), e);
514                 return;
515             }
516         }
517     }
518 
519     private <T extends HttpEntityEnclosingRequest> T entity(T request, HttpEntity entity) {
520         request.setEntity(entity);
521         return request;
522     }
523 
524     private boolean isPayloadPresent(HttpUriRequest request) {
525         if (request instanceof HttpEntityEnclosingRequest) {
526             HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
527             return entity != null && entity.getContentLength() != 0;
528         }
529         return false;
530     }
531 
532     private <T extends HttpUriRequest> T commonHeaders(T request) {
533         request.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store");
534         request.setHeader(HttpHeaders.PRAGMA, "no-cache");
535 
536         if (state.isExpectContinue() && isPayloadPresent(request)) {
537             request.setHeader(HttpHeaders.EXPECT, "100-continue");
538         }
539 
540         for (Map.Entry<?, ?> entry : headers.entrySet()) {
541             if (!(entry.getKey() instanceof String)) {
542                 continue;
543             }
544             if (entry.getValue() instanceof String) {
545                 request.setHeader(entry.getKey().toString(), entry.getValue().toString());
546             } else {
547                 request.removeHeaders(entry.getKey().toString());
548             }
549         }
550 
551         if (!state.isExpectContinue()) {
552             request.removeHeaders(HttpHeaders.EXPECT);
553         }
554 
555         return request;
556     }
557 
558     @SuppressWarnings("checkstyle:magicnumber")
559     private <T extends HttpUriRequest> T resume(T request, GetTask task) {
560         long resumeOffset = task.getResumeOffset();
561         if (resumeOffset > 0L && task.getDataFile() != null) {
562             request.setHeader(HttpHeaders.RANGE, "bytes=" + resumeOffset + '-');
563             request.setHeader(
564                     HttpHeaders.IF_UNMODIFIED_SINCE,
565                     DateUtils.formatDate(new Date(task.getDataFile().lastModified() - 60L * 1000L)));
566             request.setHeader(HttpHeaders.ACCEPT_ENCODING, "identity");
567         }
568         return request;
569     }
570 
571     @SuppressWarnings("checkstyle:magicnumber")
572     private void handleStatus(CloseableHttpResponse response) throws HttpResponseException {
573         int status = response.getStatusLine().getStatusCode();
574         if (status >= 300) {
575             throw new HttpResponseException(status, response.getStatusLine().getReasonPhrase() + " (" + status + ")");
576         }
577     }
578 
579     @Override
580     protected void implClose() {
581         try {
582             client.close();
583         } catch (IOException e) {
584             throw new UncheckedIOException(e);
585         }
586         AuthenticationContext.close(repoAuthContext);
587         AuthenticationContext.close(proxyAuthContext);
588         state.close();
589     }
590 
591     private class EntityGetter {
592 
593         private final GetTask task;
594 
595         EntityGetter(GetTask task) {
596             this.task = task;
597         }
598 
599         public void handle(CloseableHttpResponse response) throws IOException, TransferCancelledException {
600             HttpEntity entity = response.getEntity();
601             if (entity == null) {
602                 entity = new ByteArrayEntity(new byte[0]);
603             }
604 
605             long offset = 0L, length = entity.getContentLength();
606             Header rangeHeader = response.getFirstHeader(HttpHeaders.CONTENT_RANGE);
607             String range = rangeHeader != null ? rangeHeader.getValue() : null;
608             if (range != null) {
609                 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
610                 if (!m.matches()) {
611                     throw new IOException("Invalid Content-Range header for partial download: " + range);
612                 }
613                 offset = Long.parseLong(m.group(1));
614                 length = Long.parseLong(m.group(2)) + 1L;
615                 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
616                     throw new IOException("Invalid Content-Range header for partial download from offset "
617                             + task.getResumeOffset() + ": " + range);
618                 }
619             }
620 
621             final boolean resume = offset > 0L;
622             final File dataFile = task.getDataFile();
623             if (dataFile == null) {
624                 try (InputStream is = entity.getContent()) {
625                     utilGet(task, is, true, length, resume);
626                     extractChecksums(response);
627                 }
628             } else {
629                 try (FileUtils.CollocatedTempFile tempFile = FileUtils.newTempFile(dataFile.toPath())) {
630                     task.setDataFile(tempFile.getPath().toFile(), resume);
631                     if (resume && Files.isRegularFile(dataFile.toPath())) {
632                         try (InputStream inputStream = Files.newInputStream(dataFile.toPath())) {
633                             Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
634                         }
635                     }
636                     try (InputStream is = entity.getContent()) {
637                         utilGet(task, is, true, length, resume);
638                     }
639                     tempFile.move();
640                 } finally {
641                     task.setDataFile(dataFile);
642                 }
643             }
644             extractChecksums(response);
645         }
646 
647         private void extractChecksums(CloseableHttpResponse response) {
648             for (Map.Entry<String, ChecksumExtractor> extractorEntry : checksumExtractors.entrySet()) {
649                 Map<String, String> checksums = extractorEntry.getValue().extractChecksums(response);
650                 if (checksums != null) {
651                     checksums.forEach(task::setChecksum);
652                     return;
653                 }
654             }
655         }
656     }
657 
658     private class PutTaskEntity extends AbstractHttpEntity {
659 
660         private final PutTask task;
661 
662         PutTaskEntity(PutTask task) {
663             this.task = task;
664         }
665 
666         @Override
667         public boolean isRepeatable() {
668             return true;
669         }
670 
671         @Override
672         public boolean isStreaming() {
673             return false;
674         }
675 
676         @Override
677         public long getContentLength() {
678             return task.getDataLength();
679         }
680 
681         @Override
682         public InputStream getContent() throws IOException {
683             return task.newInputStream();
684         }
685 
686         @Override
687         public void writeTo(OutputStream os) throws IOException {
688             try {
689                 utilPut(task, os, false);
690             } catch (TransferCancelledException e) {
691                 throw (IOException) new InterruptedIOException().initCause(e);
692             }
693         }
694     }
695 }