1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.transport.jdk;
20
21 import javax.net.ssl.SSLContext;
22 import javax.net.ssl.SSLEngine;
23 import javax.net.ssl.SSLException;
24 import javax.net.ssl.SSLParameters;
25 import javax.net.ssl.X509ExtendedTrustManager;
26 import javax.net.ssl.X509TrustManager;
27
28 import java.io.BufferedInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.InterruptedIOException;
32 import java.io.UncheckedIOException;
33 import java.lang.reflect.InvocationTargetException;
34 import java.lang.reflect.Method;
35 import java.net.Authenticator;
36 import java.net.ConnectException;
37 import java.net.InetAddress;
38 import java.net.InetSocketAddress;
39 import java.net.NoRouteToHostException;
40 import java.net.PasswordAuthentication;
41 import java.net.ProxySelector;
42 import java.net.Socket;
43 import java.net.URI;
44 import java.net.URISyntaxException;
45 import java.net.UnknownHostException;
46 import java.net.http.HttpClient;
47 import java.net.http.HttpClient.Version;
48 import java.net.http.HttpRequest;
49 import java.net.http.HttpResponse;
50 import java.nio.file.Files;
51 import java.nio.file.Path;
52 import java.nio.file.StandardCopyOption;
53 import java.security.cert.X509Certificate;
54 import java.time.Duration;
55 import java.time.Instant;
56 import java.time.ZoneId;
57 import java.time.ZonedDateTime;
58 import java.time.format.DateTimeFormatter;
59 import java.time.format.DateTimeParseException;
60 import java.util.Base64;
61 import java.util.HashMap;
62 import java.util.Locale;
63 import java.util.Map;
64 import java.util.Objects;
65 import java.util.Optional;
66 import java.util.Set;
67 import java.util.concurrent.Semaphore;
68 import java.util.function.Function;
69 import java.util.regex.Matcher;
70
71 import com.github.mizosoft.methanol.Methanol;
72 import com.github.mizosoft.methanol.RetryInterceptor;
73 import com.github.mizosoft.methanol.RetryInterceptor.Context;
74 import org.eclipse.aether.ConfigurationProperties;
75 import org.eclipse.aether.ConfigurationProperties.HttpVersion;
76 import org.eclipse.aether.RepositorySystemSession;
77 import org.eclipse.aether.repository.AuthenticationContext;
78 import org.eclipse.aether.repository.RemoteRepository;
79 import org.eclipse.aether.spi.connector.transport.AbstractTransporter;
80 import org.eclipse.aether.spi.connector.transport.GetTask;
81 import org.eclipse.aether.spi.connector.transport.PeekTask;
82 import org.eclipse.aether.spi.connector.transport.PutTask;
83 import org.eclipse.aether.spi.connector.transport.TransportListenerNotifyingInputStream;
84 import org.eclipse.aether.spi.connector.transport.TransportTask;
85 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
86 import org.eclipse.aether.spi.connector.transport.http.HttpTransportPropertiesBuilder;
87 import org.eclipse.aether.spi.connector.transport.http.HttpTransporter;
88 import org.eclipse.aether.spi.connector.transport.http.HttpTransporterException;
89 import org.eclipse.aether.spi.io.PathProcessor;
90 import org.eclipse.aether.transfer.HttpTransportProperty;
91 import org.eclipse.aether.transfer.NoTransporterException;
92 import org.eclipse.aether.transfer.TransferCancelledException;
93 import org.eclipse.aether.transfer.TransferEvent;
94 import org.eclipse.aether.util.ConfigUtils;
95 import org.eclipse.aether.util.connector.transport.http.HttpTransporterUtils;
96 import org.slf4j.Logger;
97 import org.slf4j.LoggerFactory;
98
99 import static java.nio.charset.StandardCharsets.ISO_8859_1;
100 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.ACCEPT_ENCODING;
101 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CACHE_CONTROL;
102 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_LENGTH;
103 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE;
104 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.CONTENT_RANGE_PATTERN;
105 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.IF_UNMODIFIED_SINCE;
106 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.LAST_MODIFIED;
107 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.MULTIPLE_CHOICES;
108 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.PRECONDITION_FAILED;
109 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.RANGE;
110 import static org.eclipse.aether.spi.connector.transport.http.HttpConstants.USER_AGENT;
111 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_HTTP_VERSION;
112 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.CONFIG_PROP_MAX_CONCURRENT_REQUESTS;
113 import static org.eclipse.aether.transport.jdk.JdkTransporterConfigurationKeys.DEFAULT_MAX_CONCURRENT_REQUESTS;
114
115
116
117
118
119
120
121
122
123
124
125
126
127 final class JdkTransporter extends AbstractTransporter implements HttpTransporter {
128 private static final Logger LOGGER = LoggerFactory.getLogger(JdkTransporter.class);
129
130 private static final DateTimeFormatter RFC7231 = DateTimeFormatter.ofPattern(
131 "EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
132 .withZone(ZoneId.of("GMT"));
133
134 private static final long MODIFICATION_THRESHOLD = 60L * 1000L;
135
136
137
138
139
140
141 private static final Set<Class<? extends IOException>> NON_RETRIABLE_IO_EXCEPTIONS = Set.of(
142 InterruptedIOException.class,
143 UnknownHostException.class,
144 ConnectException.class,
145 NoRouteToHostException.class,
146 SSLException.class);
147
148 private final ChecksumExtractor checksumExtractor;
149
150 private final PathProcessor pathProcessor;
151
152 private final URI baseUri;
153
154 private final HttpClient client;
155
156 private final Map<String, String> headers;
157
158 private final int connectTimeout;
159
160 private final int requestTimeout;
161
162 private final Boolean expectContinue;
163
164 private final Semaphore maxConcurrentRequests;
165
166 private final boolean preemptivePutAuth;
167
168 private final boolean preemptiveAuth;
169
170 private final boolean sendRfc9457Accept;
171
172 private PasswordAuthentication serverAuthentication;
173
174 private PasswordAuthentication proxyAuthentication;
175
176 JdkTransporter(
177 RepositorySystemSession session,
178 RemoteRepository repository,
179 int javaVersion,
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 } catch (URISyntaxException e) {
188 throw new NoTransporterException(repository, e.getMessage(), e);
189 }
190
191 HashMap<String, String> headers = new HashMap<>();
192 String userAgent = HttpTransporterUtils.getUserAgent(session, repository);
193 if (userAgent != null) {
194 headers.put(USER_AGENT, userAgent);
195 }
196 Map<String, String> configuredHeaders = HttpTransporterUtils.getHttpHeaders(session, repository);
197 if (configuredHeaders != null) {
198 headers.putAll(configuredHeaders);
199 }
200 headers.put(CACHE_CONTROL, "no-cache, no-store");
201
202 this.connectTimeout = HttpTransporterUtils.getHttpConnectTimeout(session, repository);
203 this.requestTimeout = HttpTransporterUtils.getHttpRequestTimeout(session, repository);
204 Optional<Boolean> expectContinue = HttpTransporterUtils.getHttpExpectContinue(session, repository);
205 if (javaVersion > 19) {
206 this.expectContinue = expectContinue.orElse(null);
207 } else {
208 this.expectContinue = null;
209 if (expectContinue.isPresent()) {
210 LOGGER.warn(
211 "Configuration for Expect-Continue set but is ignored on Java versions below 20 (current java version is {}) due https://bugs.openjdk.org/browse/JDK-8286171",
212 javaVersion);
213 }
214 }
215 final String httpsSecurityMode = HttpTransporterUtils.getHttpsSecurityMode(session, repository);
216 final boolean insecure = ConfigurationProperties.HTTPS_SECURITY_MODE_INSECURE.equals(httpsSecurityMode);
217
218 this.maxConcurrentRequests = new Semaphore(ConfigUtils.getInteger(
219 session,
220 DEFAULT_MAX_CONCURRENT_REQUESTS,
221 CONFIG_PROP_MAX_CONCURRENT_REQUESTS + "." + repository.getId(),
222 CONFIG_PROP_MAX_CONCURRENT_REQUESTS));
223
224 this.preemptiveAuth = HttpTransporterUtils.isHttpPreemptiveAuth(session, repository);
225 this.preemptivePutAuth = HttpTransporterUtils.isHttpPreemptivePutAuth(session, repository);
226 this.sendRfc9457Accept = HttpTransporterUtils.isHttpSendRfc9457Accept(session, repository);
227
228 this.headers = headers;
229 this.client = createClient(session, repository, insecure);
230 }
231
232 private URI resolve(TransportTask task) {
233 return baseUri.resolve(task.getLocation());
234 }
235
236 private ConnectException enhance(ConnectException connectException) {
237 ConnectException result = new ConnectException("Connection to " + baseUri.toASCIIString() + " refused");
238 result.initCause(connectException);
239 return result;
240 }
241
242 @Override
243 protected void implPeek(PeekTask task) throws Exception {
244 HttpRequest.Builder request =
245 HttpRequest.newBuilder().uri(resolve(task)).method("HEAD", HttpRequest.BodyPublishers.noBody());
246 headers.forEach(request::setHeader);
247
248 prepare(request);
249 try {
250 HttpResponse<Void> response = send(request.build(), HttpResponse.BodyHandlers.discarding());
251 task.getListener().transportPropertiesAvailable(createTransportProperties(response));
252 if (response.statusCode() >= MULTIPLE_CHOICES) {
253 throw new HttpTransporterException(response.statusCode());
254 }
255 } catch (ConnectException e) {
256 throw enhance(e);
257 }
258 }
259
260 @Override
261 protected void implGet(GetTask task) throws Exception {
262 boolean resume = task.getResumeOffset() > 0L && task.getDataPath() != null;
263 HttpResponse<InputStream> response = null;
264
265 try {
266 while (true) {
267 HttpRequest.Builder request =
268 HttpRequest.newBuilder().uri(resolve(task)).GET();
269 headers.forEach(request::setHeader);
270 if (sendRfc9457Accept) {
271 JdkRFC9457Reporter.INSTANCE.prepareRequest(request);
272 }
273
274 if (resume) {
275 long resumeOffset = task.getResumeOffset();
276 long lastModified = pathProcessor.lastModified(task.getDataPath(), 0L);
277 request.header(RANGE, "bytes=" + resumeOffset + '-');
278 request.header(
279 IF_UNMODIFIED_SINCE,
280 RFC7231.format(Instant.ofEpochMilli(lastModified - MODIFICATION_THRESHOLD)));
281 request.header(ACCEPT_ENCODING, "identity");
282 }
283
284 prepare(request);
285 try {
286 response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
287 task.getListener().transportPropertiesAvailable(createTransportProperties(response));
288 if (response.statusCode() >= MULTIPLE_CHOICES) {
289 if (resume && response.statusCode() == PRECONDITION_FAILED) {
290 closeBody(response);
291 resume = false;
292 continue;
293 }
294 JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
295 throw new HttpTransporterException(statusCode);
296 });
297 }
298 } catch (ConnectException e) {
299 throw enhance(e);
300 }
301 break;
302 }
303
304 long offset = 0L,
305 length = response.headers().firstValueAsLong(CONTENT_LENGTH).orElse(-1L);
306 if (resume) {
307 String range = response.headers().firstValue(CONTENT_RANGE).orElse(null);
308 if (range != null) {
309 Matcher m = CONTENT_RANGE_PATTERN.matcher(range);
310 if (!m.matches()) {
311 throw new IOException("Invalid Content-Range header for partial download: " + range);
312 }
313 offset = Long.parseLong(m.group(1));
314 length = Long.parseLong(m.group(2)) + 1L;
315 if (offset < 0L || offset >= length || (offset > 0L && offset != task.getResumeOffset())) {
316 throw new IOException("Invalid Content-Range header for partial download from offset "
317 + task.getResumeOffset() + ": " + range);
318 }
319 }
320 }
321
322 final boolean downloadResumed = offset > 0L;
323 final Path dataFile = task.getDataPath();
324 if (dataFile == null) {
325 try (InputStream is = response.body()) {
326 utilGet(task, is, true, length, downloadResumed);
327 }
328 } else {
329 try (PathProcessor.CollocatedTempFile tempFile = pathProcessor.newTempFile(dataFile)) {
330 task.setDataPath(tempFile.getPath(), downloadResumed);
331 if (downloadResumed && Files.isRegularFile(dataFile)) {
332 try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(dataFile))) {
333 Files.copy(inputStream, tempFile.getPath(), StandardCopyOption.REPLACE_EXISTING);
334 }
335 }
336 try (InputStream is = response.body()) {
337 utilGet(task, is, true, length, downloadResumed);
338 }
339 tempFile.move();
340 } finally {
341 task.setDataPath(dataFile);
342 }
343 }
344 if (task.getDataPath() != null) {
345 String lastModifiedHeader = response.headers()
346 .firstValue(LAST_MODIFIED)
347 .orElse(null);
348 if (lastModifiedHeader != null) {
349 try {
350 pathProcessor.setLastModified(
351 task.getDataPath(),
352 ZonedDateTime.parse(lastModifiedHeader, RFC7231)
353 .toInstant()
354 .toEpochMilli());
355 } catch (DateTimeParseException e) {
356
357 }
358 }
359 }
360 Map<String, String> checksums = checksumExtractor.extractChecksums(headerGetter(response));
361 if (checksums != null && !checksums.isEmpty()) {
362 checksums.forEach(task::setChecksum);
363 }
364 } finally {
365 closeBody(response);
366 }
367 }
368
369 private Map<TransferEvent.TransportPropertyKey, Object> createTransportProperties(HttpResponse<?> response) {
370 HttpTransportPropertiesBuilder builder = new HttpTransportPropertiesBuilder(toHttpVersion(response.version()));
371 response.sslSession().ifPresent(ssl -> {
372 builder.withSslProtocol(ssl.getProtocol());
373 builder.withSslCipherSuite(ssl.getCipherSuite());
374 });
375
376 return builder.build();
377 }
378
379 static HttpTransportProperty.HttpVersion toHttpVersion(HttpClient.Version version) {
380 switch (version) {
381 case HTTP_1_1:
382 return HttpTransportProperty.HttpVersion.HTTP_1_1;
383 case HTTP_2:
384 return HttpTransportProperty.HttpVersion.HTTP_2;
385 default:
386
387 if ("HTTP_3".equals(version.name())) {
388 return HttpTransportProperty.HttpVersion.HTTP_3;
389 } else {
390 throw new IllegalArgumentException("Unsupported HTTP version: " + version);
391 }
392 }
393 }
394
395 private static Function<String, String> headerGetter(HttpResponse<?> response) {
396 return s -> response.headers().firstValue(s).orElse(null);
397 }
398
399 private void closeBody(HttpResponse<InputStream> streamHttpResponse) throws IOException {
400 if (streamHttpResponse != null) {
401 InputStream body = streamHttpResponse.body();
402 if (body != null) {
403 body.close();
404 }
405 }
406 }
407
408 @Override
409 protected void implPut(PutTask task) throws Exception {
410 HttpRequest.Builder request = HttpRequest.newBuilder().uri(resolve(task));
411 if (expectContinue != null) {
412 request = request.expectContinue(expectContinue);
413 }
414 headers.forEach(request::setHeader);
415 if (sendRfc9457Accept) {
416 JdkRFC9457Reporter.INSTANCE.prepareRequest(request);
417 }
418 if (task.getDataLength() == 0L) {
419 request.PUT(HttpRequest.BodyPublishers.noBody());
420 } else {
421 request.PUT(HttpRequest.BodyPublishers.fromPublisher(
422 HttpRequest.BodyPublishers.ofInputStream(() -> {
423 try {
424
425 return new TransportListenerNotifyingInputStream(
426 task.newInputStream(), task.getListener(), task.getDataLength());
427 } catch (IOException e) {
428 throw new UncheckedIOException(e);
429 }
430 }),
431
432 task.getDataLength()));
433 }
434 prepare(request);
435 HttpResponse<InputStream> response = null;
436 try {
437 response = send(request.build(), HttpResponse.BodyHandlers.ofInputStream());
438 task.getListener().transportPropertiesAvailable(createTransportProperties(response));
439 if (response.statusCode() >= MULTIPLE_CHOICES) {
440 JdkRFC9457Reporter.INSTANCE.generateException(response, (statusCode, reasonPhrase) -> {
441 throw new HttpTransporterException(statusCode);
442 });
443 }
444 } catch (ConnectException e) {
445 throw enhance(e);
446 } catch (IOException e) {
447
448 Throwable rootCause = getRootCause(e);
449 if (rootCause instanceof TransferCancelledException) {
450 throw (TransferCancelledException) rootCause;
451 }
452 throw e;
453 } finally {
454 closeBody(response);
455 }
456 }
457
458 private void prepare(HttpRequest.Builder requestBuilder) {
459 if (preemptiveAuth
460 || (preemptivePutAuth && requestBuilder.build().method().equals("PUT"))) {
461 if (serverAuthentication != null) {
462
463 requestBuilder.setHeader(
464 "Authorization",
465 getBasicAuthValue(serverAuthentication.getUserName(), serverAuthentication.getPassword()));
466 }
467 if (proxyAuthentication != null) {
468 requestBuilder.setHeader(
469 "Proxy-Authorization",
470 getBasicAuthValue(proxyAuthentication.getUserName(), proxyAuthentication.getPassword()));
471 }
472 }
473 }
474
475 static String getBasicAuthValue(String username, char[] password) {
476
477 return "Basic "
478 + Base64.getEncoder().encodeToString((username + ':' + String.valueOf(password)).getBytes(ISO_8859_1));
479 }
480
481 private <T> HttpResponse<T> send(HttpRequest request, HttpResponse.BodyHandler<T> responseBodyHandler)
482 throws Exception {
483 maxConcurrentRequests.acquire();
484 try {
485 return client.send(request, responseBodyHandler);
486 } finally {
487 maxConcurrentRequests.release();
488 }
489 }
490
491 @Override
492 protected void implClose() {
493 if (client != null) {
494 JdkTransporterCloser.closer(client).run();
495 }
496 }
497
498 HttpClient.Version getHttpVersion(RepositorySystemSession session, RemoteRepository repository) {
499 HttpVersion httpVersion = HttpTransporterUtils.getHttpVersion(session, repository);
500 if (httpVersion == ConfigurationProperties.DEFAULT_HTTP_VERSION) {
501
502 String configuredLegacyHttpVersion = ConfigUtils.getString(
503 session, null, CONFIG_PROP_HTTP_VERSION + "." + repository.getId(), CONFIG_PROP_HTTP_VERSION);
504 if (configuredLegacyHttpVersion != null) {
505 return resolveHttpVersion(configuredLegacyHttpVersion);
506 }
507 return HttpClient.Version.HTTP_2;
508 } else {
509 switch (httpVersion) {
510 case MAXIMUM:
511 return getMaximumSupportedHttpVersion();
512 case HTTP_1_1:
513 return HttpClient.Version.HTTP_1_1;
514 case HTTP_2:
515 case DEFAULT:
516 return HttpClient.Version.HTTP_2;
517 case HTTP_3:
518 return resolveHttpVersion("HTTP_3");
519 default:
520
521 throw new IllegalStateException("Unknown HTTP version: " + httpVersion);
522 }
523 }
524 }
525
526 private HttpClient.Version resolveHttpVersion(String requestedVersion) {
527 try {
528 return HttpClient.Version.valueOf(requestedVersion);
529 } catch (IllegalArgumentException e) {
530 HttpClient.Version maximumHttpVersion = getMaximumSupportedHttpVersion();
531 LOGGER.warn(
532 "HTTP version '{}' is not supported by the running JRE, using '{}' instead",
533 requestedVersion,
534 maximumHttpVersion);
535 return maximumHttpVersion;
536 }
537 }
538
539 HttpClient.Version getMaximumSupportedHttpVersion() {
540 HttpClient.Version[] values = HttpClient.Version.values();
541 return values[values.length - 1];
542 }
543
544 private HttpClient createClient(RepositorySystemSession session, RemoteRepository repository, boolean insecure)
545 throws RuntimeException {
546
547 HashMap<Authenticator.RequestorType, PasswordAuthentication> authentications = new HashMap<>();
548 SSLContext sslContext = null;
549 try (AuthenticationContext repoAuthContext = AuthenticationContext.forRepository(session, repository)) {
550 if (repoAuthContext != null) {
551 sslContext = repoAuthContext.get(AuthenticationContext.SSL_CONTEXT, SSLContext.class);
552
553 String username = repoAuthContext.get(AuthenticationContext.USERNAME);
554 String password = repoAuthContext.get(AuthenticationContext.PASSWORD);
555 serverAuthentication = new PasswordAuthentication(username, password.toCharArray());
556 authentications.put(Authenticator.RequestorType.SERVER, serverAuthentication);
557 }
558 }
559
560 Version httpVersion = getHttpVersion(session, repository);
561 if (sslContext == null) {
562 try {
563 if (insecure) {
564 if (httpVersion.name().equals("HTTP_3")) {
565
566
567
568
569 throw new IllegalStateException(
570 "Insecure HTTPS connections are not supported for HTTP/3 (Quic)");
571 }
572 sslContext = SSLContext.getInstance("TLS");
573 X509ExtendedTrustManager tm = new X509ExtendedTrustManager() {
574 @Override
575 public void checkClientTrusted(X509Certificate[] chain, String authType) {}
576
577 @Override
578 public void checkServerTrusted(X509Certificate[] chain, String authType) {}
579
580 @Override
581 public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) {}
582
583 @Override
584 public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) {}
585
586 @Override
587 public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
588
589 @Override
590 public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) {}
591
592 @Override
593 public X509Certificate[] getAcceptedIssuers() {
594 return null;
595 }
596 };
597 sslContext.init(null, new X509TrustManager[] {tm}, null);
598 } else {
599 sslContext = SSLContext.getDefault();
600 }
601 } catch (Exception e) {
602 if (e instanceof RuntimeException) {
603 throw (RuntimeException) e;
604 } else {
605 throw new IllegalStateException("SSL Context setup failure", e);
606 }
607 }
608 } else {
609 if (insecure) {
610 throw new IllegalStateException(
611 "Insecure HTTPS connections are not supported when a custom SSLContext is configured");
612 }
613 }
614
615 Methanol.Builder builder = Methanol.newBuilder()
616 .version(httpVersion)
617 .followRedirects(HttpClient.Redirect.NORMAL)
618 .connectTimeout(Duration.ofMillis(connectTimeout))
619
620
621
622 .requestTimeout(Duration.ofMillis(requestTimeout))
623 .sslContext(sslContext);
624
625 if (insecure) {
626 SSLParameters sslParameters = sslContext.getDefaultSSLParameters();
627 sslParameters.setEndpointIdentificationAlgorithm(null);
628 builder.sslParameters(sslParameters);
629 }
630
631 setLocalAddress(
632 builder,
633 HttpTransporterUtils.getHttpLocalAddress(session, repository).orElse(null));
634
635 if (repository.getProxy() != null) {
636 InetSocketAddress proxyAddress = new InetSocketAddress(
637 repository.getProxy().getHost(), repository.getProxy().getPort());
638 if (proxyAddress.isUnresolved()) {
639 throw new IllegalStateException(
640 "Proxy host " + repository.getProxy().getHost() + " could not be resolved");
641 }
642 builder.proxy(ProxySelector.of(proxyAddress));
643 try (AuthenticationContext proxyAuthContext = AuthenticationContext.forProxy(session, repository)) {
644 if (proxyAuthContext != null) {
645 String username = proxyAuthContext.get(AuthenticationContext.USERNAME);
646 String password = proxyAuthContext.get(AuthenticationContext.PASSWORD);
647
648 proxyAuthentication = new PasswordAuthentication(username, password.toCharArray());
649 authentications.put(Authenticator.RequestorType.PROXY, proxyAuthentication);
650 }
651 }
652 }
653
654 if (!authentications.isEmpty()) {
655 builder.authenticator(new Authenticator() {
656 @Override
657 protected PasswordAuthentication getPasswordAuthentication() {
658 return authentications.get(getRequestorType());
659 }
660 });
661 }
662
663 configureRetryHandler(session, repository, builder);
664
665 return builder.build();
666 }
667
668 private static class RetryLoggingListener implements RetryInterceptor.Listener {
669 private final int maxNumRetries;
670
671 RetryLoggingListener(int maxNumRetries) {
672 this.maxNumRetries = maxNumRetries;
673 }
674
675 @Override
676 public void onRetry(Context<?> context, HttpRequest nextRequest, Duration delay) {
677 LOGGER.warn(
678 "{} request to {} failed (attempt {} of {}) due to {}. Retrying in {} ms...",
679 context.request().method(),
680 context.request().uri(),
681 context.retryCount() + 1,
682 maxNumRetries + 1,
683 getReason(context),
684 delay.toMillis());
685 }
686
687 String getReason(Context<?> context) {
688 if (context.exception().isPresent()) {
689 return context.exception().get().getMessage();
690 } else if (context.response().isPresent()) {
691 return "status " + context.response().get().statusCode();
692 }
693
694 throw new IllegalStateException("No exception or response present in retry context");
695 }
696 }
697
698 private static void configureRetryHandler(
699 RepositorySystemSession session, RemoteRepository repository, Methanol.Builder builder) {
700 int retryCount = HttpTransporterUtils.getHttpRetryHandlerCount(session, repository);
701 long retryInterval = HttpTransporterUtils.getHttpRetryHandlerInterval(session, repository);
702 long retryIntervalMax = HttpTransporterUtils.getHttpRetryHandlerIntervalMax(session, repository);
703 if (retryCount > 0) {
704 Methanol.Interceptor rateLimitingRetryInterceptor = RetryInterceptor.newBuilder()
705 .maxRetries(retryCount)
706 .onStatus(HttpTransporterUtils.getHttpServiceUnavailableCodes(session, repository)::contains)
707 .listener(new RetryLoggingListener(retryCount))
708 .backoff(RetryInterceptor.BackoffStrategy.retryAfterOr(RetryInterceptor.BackoffStrategy.linear(
709 Duration.ofMillis(retryInterval), Duration.ofMillis(retryIntervalMax))))
710 .build();
711 builder.interceptor(rateLimitingRetryInterceptor);
712 Methanol.Interceptor retryIoExceptionsInterceptor = RetryInterceptor.newBuilder()
713
714
715
716 .maxRetries(retryCount)
717 .onException(t -> {
718
719
720
721 Throwable rootCause = getRootCause(t);
722 return t instanceof IOException
723 && !NON_RETRIABLE_IO_EXCEPTIONS.contains(t.getClass())
724 && !(rootCause instanceof TransferCancelledException);
725 })
726 .listener(new RetryLoggingListener(retryCount))
727 .build();
728 builder.interceptor(retryIoExceptionsInterceptor);
729 }
730 }
731
732 private static void setLocalAddress(HttpClient.Builder builder, InetAddress address) {
733 if (address == null) {
734 return;
735 }
736 try {
737 final Method mtd = builder.getClass().getDeclaredMethod("localAddress", InetAddress.class);
738 if (!mtd.canAccess(builder)) {
739 mtd.setAccessible(true);
740 }
741 mtd.invoke(builder, address);
742 } catch (final NoSuchMethodException ignore) {
743
744 } catch (InvocationTargetException e) {
745 throw new IllegalStateException(e.getTargetException());
746 } catch (IllegalAccessException e) {
747 throw new IllegalStateException(e);
748 }
749 }
750
751 private static Throwable getRootCause(Throwable throwable) {
752 Objects.requireNonNull(throwable);
753 Throwable rootCause = throwable;
754 while (rootCause.getCause() != null && rootCause.getCause() != rootCause) {
755 rootCause = rootCause.getCause();
756 }
757 return rootCause;
758 }
759 }